use of io.vertx.core.Future in project hono by eclipse.
the class HonoConnectionImplTest method testCreateSenderFailsOnDisconnectBeforeOpen.
/**
* Verifies that the attempt to create a sender fails with a
* {@code ServerErrorException} if the connection gets disconnected
* before the remote peer has sent its attach frame. It is verified
* that this is done before the link establishment timeout.
*
* @param ctx The vert.x test context.
*/
@Test
public void testCreateSenderFailsOnDisconnectBeforeOpen(final VertxTestContext ctx) {
// choose a distinct value here
final long linkEstablishmentTimeout = 444L;
props.setLinkEstablishmentTimeout(linkEstablishmentTimeout);
// don't run linkEstablishmentTimeout timer handler
when(vertx.setTimer(eq(linkEstablishmentTimeout), VertxMockSupport.anyHandler())).thenAnswer(invocation -> 0L);
final ProtonSender sender = mock(ProtonSender.class);
when(sender.isOpen()).thenReturn(Boolean.TRUE);
when(session.createSender(anyString())).thenReturn(sender);
final Target target = new Target();
target.setAddress("someAddress");
when(sender.getRemoteTarget()).thenReturn(target);
when(sender.getCredit()).thenReturn(0);
// mock handlers
final Handler<String> remoteCloseHook = VertxMockSupport.mockHandler();
// GIVEN an established connection
honoConnection.connect().compose(c -> {
// WHEN creating a sender link with a close hook
final Future<ProtonSender> result = honoConnection.createSender("target", ProtonQoS.AT_LEAST_ONCE, remoteCloseHook);
// THEN the result is not completed at first
ctx.verify(() -> assertThat(result.isComplete()).isFalse());
// WHEN the downstream connection fails
connectionFactory.getDisconnectHandler().handle(con);
return result;
}).onComplete(ctx.failing(t -> {
ctx.verify(() -> assertThat(((ServerErrorException) t).getErrorCode()).isEqualTo(HttpURLConnection.HTTP_UNAVAILABLE));
ctx.completeNow();
}));
}
use of io.vertx.core.Future in project hono by eclipse.
the class HonoConnectionImplTest method testCreateSenderFails.
private void testCreateSenderFails(final VertxTestContext ctx, final Supplier<ErrorCondition> errorSupplier, final Predicate<Throwable> failureAssertion) {
final ProtonSender sender = mock(ProtonSender.class);
when(sender.getRemoteCondition()).thenReturn(errorSupplier.get());
when(session.createSender(anyString())).thenReturn(sender);
final Handler<String> remoteCloseHook = VertxMockSupport.mockHandler();
when(vertx.setTimer(anyLong(), VertxMockSupport.anyHandler())).thenAnswer(invocation -> {
// do not run timers immediately
return 0L;
});
// GIVEN an established connection
honoConnection.connect().compose(c -> {
final Future<ProtonSender> s = honoConnection.createSender("target", ProtonQoS.AT_LEAST_ONCE, remoteCloseHook);
ctx.verify(() -> {
verify(vertx).setTimer(eq(props.getLinkEstablishmentTimeout()), VertxMockSupport.anyHandler());
final ArgumentCaptor<Handler<AsyncResult<ProtonSender>>> openHandler = VertxMockSupport.argumentCaptorHandler();
verify(sender).openHandler(openHandler.capture());
openHandler.getValue().handle(Future.failedFuture(new IllegalStateException()));
});
return s;
}).onComplete(ctx.failing(t -> {
ctx.verify(() -> {
assertThat(failureAssertion.test(t)).isTrue();
verify(remoteCloseHook, never()).handle(anyString());
});
ctx.completeNow();
}));
}
use of io.vertx.core.Future in project hono by eclipse.
the class ProtonBasedRequestResponseCommandClient method sendCommand.
/**
* Sends a command to a device and expects a response.
* <p>
* A device needs to be (successfully) registered before a client can upload
* any data for it. The device also needs to be connected to a protocol adapter
* and needs to have indicated its intent to receive commands.
*
* @param tenantId The tenant that the device belongs to.
* @param deviceId The device to send the command to.
* @param command The name of the command.
* @param contentType The type of the data submitted as part of the command or {@code null} if unknown.
* @param data The input data to the command or {@code null} if the command has no input data.
* @param replyId An arbitrary string which gets used for the response link address in the form of
* <em>command_response/${tenantId}/${replyId}</em>. If it is {@code null} then an unique
* identifier generated using {@link UUID#randomUUID()} is used.
* @param properties The headers to include in the command message as AMQP application properties.
* @param timeout The duration after which the send command request times out. If the timeout is {@code null}
* then the default timeout value of {@value DEFAULT_COMMAND_TIMEOUT_IN_MS} ms is used.
* If the timeout duration is set to 0 then the send command request never times out.
* @param context The currently active OpenTracing span context that is used to trace the execution of this
* operation or {@code null} if no span is currently active.
* @return A future indicating the result of the operation.
* <p>
* The future will succeed if a response with status 2xx has been received from the device.
* If the response has no payload, the future will complete with a DownstreamMessage that has a {@code null} payload.
* <p>
* Otherwise, the future will fail with a {@link ServiceInvocationException} containing
* the (error) status code. Status codes are defined at
* <a href="https://www.eclipse.org/hono/docs/api/command-and-control">Command and Control API</a>.
* @throws NullPointerException if any of tenantId, deviceId or command are {@code null}.
* @throws IllegalArgumentException if the timeout duration value is < 0
*/
public Future<DownstreamMessage<AmqpMessageContext>> sendCommand(final String tenantId, final String deviceId, final String command, final String contentType, final Buffer data, final String replyId, final Map<String, Object> properties, final Duration timeout, final SpanContext context) {
Objects.requireNonNull(tenantId);
Objects.requireNonNull(deviceId);
Objects.requireNonNull(command);
final long timeoutInMs = Optional.ofNullable(timeout).map(t -> {
if (t.isNegative()) {
throw new IllegalArgumentException("command timeout duration must be >= 0");
}
return t.toMillis();
}).orElse(DEFAULT_COMMAND_TIMEOUT_IN_MS);
final Span currentSpan = newChildSpan(context, "send command and receive response");
return getOrCreateClient(tenantId, replyId).map(client -> {
client.setRequestTimeout(timeoutInMs);
return client;
}).compose(client -> {
final String messageTargetAddress = AddressHelper.getTargetAddress(CommandConstants.NORTHBOUND_COMMAND_REQUEST_ENDPOINT, tenantId, deviceId, connection.getConfig());
return client.createAndSendRequest(command, messageTargetAddress, properties, data, contentType, this::mapCommandResponse, currentSpan);
}).recover(error -> {
Tags.HTTP_STATUS.set(currentSpan, ServiceInvocationException.extractStatusCode(error));
TracingHelper.logError(currentSpan, error);
return Future.failedFuture(error);
}).compose(result -> {
if (result == null) {
return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST));
} else {
final DownstreamMessage<AmqpMessageContext> commandResponseMessage = result.getPayload();
setTagsForResult(currentSpan, result);
if (result.isError()) {
final String detailMessage = commandResponseMessage.getPayload() != null && commandResponseMessage.getPayload().length() > 0 ? commandResponseMessage.getPayload().toString(StandardCharsets.UTF_8) : null;
return Future.failedFuture(StatusCodeMapper.from(result.getStatus(), detailMessage));
}
return Future.succeededFuture(commandResponseMessage);
}
}).onComplete(r -> currentSpan.finish());
}
use of io.vertx.core.Future in project hono by eclipse.
the class ProtonBasedCommandResponseSender method sendCommandResponse.
@Override
public Future<Void> sendCommandResponse(final TenantObject tenant, final RegistrationAssertion device, final CommandResponse response, final SpanContext context) {
Objects.requireNonNull(tenant);
Objects.requireNonNull(device);
Objects.requireNonNull(response);
final var sender = createSender(response.getTenantId(), response.getReplyToId());
return sender.recover(thr -> Future.failedFuture(StatusCodeMapper.toServerError(thr))).compose(s -> {
final Message msg = createDownstreamMessage(response, tenant, device, response.getAdditionalProperties());
final Span span = newChildSpan(context, "forward Command response");
if (response.getMessagingType() != getMessagingType()) {
span.log(String.format("using messaging type %s instead of type %s used for the original command", getMessagingType(), response.getMessagingType()));
}
return s.sendAndWaitForOutcome(msg, span);
}).onSuccess(delivery -> sender.result().close()).mapEmpty();
}
use of io.vertx.core.Future in project hono by eclipse.
the class KafkaBasedCommandSender method subscribeForCommandResponse.
private Future<Void> subscribeForCommandResponse(final String tenantId, final Span span) {
if (commandResponseConsumers.get(tenantId) != null) {
LOGGER.debug("command response consumer already exists for tenant [{}]", tenantId);
span.log("command response consumer already exists");
return Future.succeededFuture();
}
final Map<String, String> consumerConfig = this.consumerConfig.getConsumerConfig(HonoTopic.Type.COMMAND_RESPONSE.toString());
final String autoOffsetResetConfigValue = consumerConfig.get(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG);
// Ensure that 'auto.offset.reset' is always set to 'latest'.
if (autoOffsetResetConfigValue != null && !autoOffsetResetConfigValue.equals("latest")) {
LOGGER.warn("[auto.offset.reset] value is set to other than [latest]. It will be ignored and internally set to [latest]");
}
consumerConfig.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
// Use a unique group-id so that all command responses for this tenant are received by this consumer.
// Thereby the responses can be correlated with the command that has been sent.
consumerConfig.put(ConsumerConfig.GROUP_ID_CONFIG, tenantId + "-" + UUID.randomUUID());
final String topic = new HonoTopic(HonoTopic.Type.COMMAND_RESPONSE, tenantId).toString();
final Handler<KafkaConsumerRecord<String, Buffer>> recordHandler = record -> {
getCommandResponseHandler(tenantId).handle(new KafkaDownstreamMessage(record));
};
final HonoKafkaConsumer consumer = new HonoKafkaConsumer(vertx, Set.of(topic), recordHandler, consumerConfig);
consumer.setPollTimeout(Duration.ofMillis(this.consumerConfig.getPollTimeout()));
Optional.ofNullable(kafkaConsumerSupplier).ifPresent(consumer::setKafkaConsumerSupplier);
return consumer.start().recover(error -> {
LOGGER.debug("error creating command response consumer for tenant [{}]", tenantId, error);
TracingHelper.logError(span, "error creating command response consumer", error);
return Future.failedFuture(error);
}).onSuccess(v -> {
LOGGER.debug("created command response consumer for tenant [{}]", tenantId);
span.log("created command response consumer");
commandResponseConsumers.put(tenantId, consumer);
});
}
Aggregations