use of io.vertx.core.Future in project hono by eclipse.
the class GenericSenderLink method sendMessageAndWaitForOutcome.
private Future<ProtonDelivery> sendMessageAndWaitForOutcome(final Message message, final Span currentSpan, final boolean mapUnacceptedOutcomeToErrorResult) {
Objects.requireNonNull(message);
Objects.requireNonNull(currentSpan);
final AtomicReference<ProtonDelivery> deliveryRef = new AtomicReference<>();
final Promise<ProtonDelivery> result = Promise.promise();
final String messageId = String.format("%s-%d", getClass().getSimpleName(), MESSAGE_COUNTER.getAndIncrement());
message.setMessageId(messageId);
logMessageIdAndSenderInfo(currentSpan, messageId);
final SendMessageSampler.Sample sample = sampler.start(tenantId);
final ClientConfigProperties config = connection.getConfig();
final Long timerId = config.getSendMessageTimeout() > 0 ? connection.getVertx().setTimer(config.getSendMessageTimeout(), id -> {
if (!result.future().isComplete()) {
handleSendMessageTimeout(message, config.getSendMessageTimeout(), deliveryRef.get(), sample, result, null);
}
}) : null;
deliveryRef.set(sender.send(message, deliveryUpdated -> {
if (timerId != null) {
connection.getVertx().cancelTimer(timerId);
}
final DeliveryState remoteState = deliveryUpdated.getRemoteState();
if (result.future().isComplete()) {
log.debug("ignoring received delivery update for message [ID: {}, address: {}]: waiting for the update has already timed out", messageId, getMessageAddress(message));
} else if (deliveryUpdated.remotelySettled()) {
logUpdatedDeliveryState(currentSpan, message, deliveryUpdated);
sample.completed(remoteState);
if (Accepted.class.isInstance(remoteState)) {
result.complete(deliveryUpdated);
} else {
if (mapUnacceptedOutcomeToErrorResult) {
result.handle(mapUnacceptedOutcomeToErrorResult(deliveryUpdated));
} else {
result.complete(deliveryUpdated);
}
}
} else {
logMessageSendingError("peer did not settle message [ID: {}, address: {}, remote state: {}], failing delivery", messageId, getMessageAddress(message), remoteState.getClass().getSimpleName());
final ServiceInvocationException e = new ServerErrorException(HttpURLConnection.HTTP_INTERNAL_ERROR, "peer did not settle message, failing delivery");
result.fail(e);
}
}));
log.trace("sent AT_LEAST_ONCE message [ID: {}, address: {}], remaining credit: {}, queued messages: {}", messageId, getMessageAddress(message), sender.getCredit(), sender.getQueued());
return result.future().onSuccess(delivery -> Tags.HTTP_STATUS.set(currentSpan, HttpURLConnection.HTTP_ACCEPTED)).onFailure(t -> {
TracingHelper.logError(currentSpan, t);
Tags.HTTP_STATUS.set(currentSpan, ServiceInvocationException.extractStatusCode(t));
}).onComplete(r -> currentSpan.finish());
}
use of io.vertx.core.Future in project hono by eclipse.
the class KafkaBasedCommandConsumerFactoryImplIT method testCommandsGetForwardedInIncomingOrder.
/**
* Verifies that records, published on the tenant-specific Kafka command topic, get received by
* the consumer created by the factory and get forwarded on the internal command topic in the
* same order they were published.
*
* @param ctx The vert.x test context.
* @throws InterruptedException if test execution gets interrupted.
*/
@Test
@Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
public void testCommandsGetForwardedInIncomingOrder(final VertxTestContext ctx) throws InterruptedException {
final String tenantId = "tenant_" + UUID.randomUUID();
final VertxTestContext setup = new VertxTestContext();
final int numTestCommands = 10;
final List<KafkaConsumerRecord<String, Buffer>> receivedRecords = new ArrayList<>();
final Promise<Void> allRecordsReceivedPromise = Promise.promise();
final List<String> receivedCommandSubjects = new ArrayList<>();
final Handler<KafkaConsumerRecord<String, Buffer>> recordHandler = record -> {
receivedRecords.add(record);
LOG.trace("received {}", record);
receivedCommandSubjects.add(KafkaRecordHelper.getSubject(record.headers()).orElse(""));
if (receivedRecords.size() == numTestCommands) {
allRecordsReceivedPromise.tryComplete();
}
};
final Deque<Promise<Void>> completionPromisesQueue = new LinkedList<>();
// don't let getting the target adapter instance finish immediately
// - let the futures complete in the reverse order
final Supplier<Future<Void>> targetAdapterInstanceGetterCompletionFutureSupplier = () -> {
final Promise<Void> resultPromise = Promise.promise();
completionPromisesQueue.addFirst(resultPromise);
// complete all promises in reverse order when processing the last command
if (completionPromisesQueue.size() == numTestCommands) {
completionPromisesQueue.forEach(Promise::complete);
}
return resultPromise.future();
};
final Context vertxContext = vertx.getOrCreateContext();
vertxContext.runOnContext(v0 -> {
final HonoKafkaConsumer internalConsumer = getInternalCommandConsumer(recordHandler);
final KafkaBasedCommandConsumerFactoryImpl consumerFactory = getKafkaBasedCommandConsumerFactory(targetAdapterInstanceGetterCompletionFutureSupplier, tenantId);
CompositeFuture.join(internalConsumer.start(), consumerFactory.start()).compose(f -> createCommandConsumer(tenantId, consumerFactory)).onComplete(setup.succeedingThenComplete());
});
assertThat(setup.awaitCompletion(IntegrationTestSupport.getTestSetupTimeout(), TimeUnit.SECONDS)).isTrue();
if (setup.failed()) {
ctx.failNow(setup.causeOfFailure());
return;
}
LOG.debug("command consumer started");
final List<String> sentCommandSubjects = new ArrayList<>();
IntStream.range(0, numTestCommands).forEach(i -> {
final String subject = "cmd_" + i;
sentCommandSubjects.add(subject);
sendOneWayCommand(tenantId, "myDeviceId", subject);
});
final long timerId = vertx.setTimer(8000, tid -> {
LOG.info("received records:{}{}", System.lineSeparator(), receivedRecords.stream().map(Object::toString).collect(Collectors.joining("," + System.lineSeparator())));
allRecordsReceivedPromise.tryFail(String.format("only received %d out of %d expected messages after 8s", receivedRecords.size(), numTestCommands));
});
allRecordsReceivedPromise.future().onComplete(ctx.succeeding(v -> {
vertx.cancelTimer(timerId);
ctx.verify(() -> {
assertThat(receivedCommandSubjects).isEqualTo(sentCommandSubjects);
});
ctx.completeNow();
}));
}
use of io.vertx.core.Future in project hono by eclipse.
the class HttpTestBase method testUploadMessagesWithTtdThatReplyWithCommand.
private void testUploadMessagesWithTtdThatReplyWithCommand(final HttpCommandEndpointConfiguration endpointConfig, final Tenant tenant, final VertxTestContext ctx) throws InterruptedException {
final VertxTestContext setup = new VertxTestContext();
final MultiMap requestHeaders = MultiMap.caseInsensitiveMultiMap().add(HttpHeaders.CONTENT_TYPE, "text/plain").add(HttpHeaders.AUTHORIZATION, authorization).add(HttpHeaders.ORIGIN, ORIGIN_URI).add(Constants.HEADER_TIME_TILL_DISCONNECT, "5");
final MultiMap cmdResponseRequestHeaders = MultiMap.caseInsensitiveMultiMap().add(HttpHeaders.CONTENT_TYPE, "text/plain").add(HttpHeaders.AUTHORIZATION, authorization).add(HttpHeaders.ORIGIN, ORIGIN_URI).add(Constants.HEADER_COMMAND_RESPONSE_STATUS, "200");
helper.registry.addDeviceForTenant(tenantId, tenant, deviceId, PWD).onComplete(setup.succeedingThenComplete());
assertThat(setup.awaitCompletion(5, TimeUnit.SECONDS)).isTrue();
if (setup.failed()) {
ctx.failNow(setup.causeOfFailure());
return;
}
final String commandTargetDeviceId = endpointConfig.isSubscribeAsGateway() ? helper.setupGatewayDeviceBlocking(tenantId, deviceId, 5) : deviceId;
final String subscribingDeviceId = endpointConfig.isSubscribeAsGatewayForSingleDevice() ? commandTargetDeviceId : deviceId;
testUploadMessages(ctx, tenantId, msg -> {
return msg.getTimeUntilDisconnectNotification().map(notification -> {
logger.trace("received piggy backed message [ttd: {}]: {}", notification.getTtd(), msg);
ctx.verify(() -> {
assertThat(notification.getTenantId()).isEqualTo(tenantId);
assertThat(notification.getDeviceId()).isEqualTo(subscribingDeviceId);
});
// now ready to send a command
final JsonObject inputData = new JsonObject().put(COMMAND_JSON_KEY, (int) (Math.random() * 100));
return helper.sendCommand(tenantId, commandTargetDeviceId, COMMAND_TO_SEND, "application/json", inputData.toBuffer(), notification.getMillisecondsUntilExpiry()).map(response -> {
ctx.verify(() -> {
assertThat(response.getContentType()).isEqualTo("text/plain");
assertThat(response.getDeviceId()).isEqualTo(commandTargetDeviceId);
assertThat(response.getTenantId()).isEqualTo(tenantId);
assertThat(response.getCreationTime()).isNotNull();
});
return (Void) null;
});
}).orElseGet(Future::succeededFuture);
}, count -> {
final Buffer buffer = Buffer.buffer("hello " + count);
return sendHttpRequestForGatewayOrDevice(buffer, requestHeaders, endpointConfig, commandTargetDeviceId, true).map(httpResponse -> {
final String requestId = httpResponse.getHeader(Constants.HEADER_COMMAND_REQUEST_ID);
ctx.verify(() -> {
// assert that the response contains a command
assertWithMessage("response no. %s '%s' header", count, Constants.HEADER_COMMAND).that(httpResponse.getHeader(Constants.HEADER_COMMAND)).isNotNull();
assertThat(httpResponse.getHeader(Constants.HEADER_COMMAND)).isEqualTo(COMMAND_TO_SEND);
assertThat(httpResponse.getHeader(HttpHeaders.CONTENT_TYPE.toString())).isEqualTo("application/json");
assertThat(requestId).isNotNull();
assertThat(httpResponse.getHeader(HttpHeaders.CONTENT_LENGTH.toString())).isNotEqualTo("0");
});
return requestId;
}).compose(receivedCommandRequestId -> {
// send a response to the command now
final String responseUri = endpointConfig.getCommandResponseUri(tenantId, commandTargetDeviceId, receivedCommandRequestId);
logger.debug("sending response to command [uri: {}]", responseUri);
final Buffer body = Buffer.buffer("ok");
final Future<HttpResponse<Buffer>> result;
if (endpointConfig.isSubscribeAsGateway()) {
// GW uses PUT when acting on behalf of a device
result = httpClient.update(responseUri, body, cmdResponseRequestHeaders, ResponsePredicate.status(HttpURLConnection.HTTP_ACCEPTED));
} else {
result = httpClient.create(responseUri, body, cmdResponseRequestHeaders, ResponsePredicate.status(HttpURLConnection.HTTP_ACCEPTED));
}
return result.recover(thr -> {
// wrap exception, making clear it occurred when sending the command response, not the preceding telemetry/event message
final String msg = "Error sending command response: " + thr.getMessage();
return Future.failedFuture(new RuntimeException(msg, thr));
});
});
});
}
use of io.vertx.core.Future in project hono by eclipse.
the class JmsBasedCredentialsClient method sendRequest.
/**
* Sends a request for an operation.
*
* @param tenantId The tenant to send the message for.
* @param operation The name of the operation to invoke or {@code null} if the message
* should not have a subject.
* @param applicationProperties Application properties to set on the request message or
* {@code null} if no properties should be set.
* @param payload Payload to include or {@code null} if the message should have no body.
* @return A future indicating the outcome of the operation.
*/
public Future<CredentialsObject> sendRequest(final String tenantId, final String operation, final Map<String, Object> applicationProperties, final Buffer payload) {
final Message request;
try {
request = createMessage(payload);
addProperties(request, applicationProperties);
if (operation != null) {
request.setJMSType(operation);
}
if (applicationProperties != null) {
for (Map.Entry<String, Object> entry : applicationProperties.entrySet()) {
if (entry.getValue() instanceof String) {
request.setStringProperty(entry.getKey(), (String) entry.getValue());
} else {
request.setObjectProperty(entry.getKey(), entry.getValue());
}
}
}
} catch (final JMSException e) {
return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, e));
}
final Future<JmsBasedRequestResponseClient<CredentialsResult<CredentialsObject>>> client = JmsBasedRequestResponseClient.forEndpoint(connection, CredentialsConstants.CREDENTIALS_ENDPOINT, tenantId);
return client.compose(c -> c.send(request, this::getResult)).compose(credentialsResult -> {
final Promise<CredentialsObject> result = Promise.promise();
switch(credentialsResult.getStatus()) {
case HttpURLConnection.HTTP_OK:
case HttpURLConnection.HTTP_CREATED:
result.complete(credentialsResult.getPayload());
break;
case HttpURLConnection.HTTP_NOT_FOUND:
result.fail(new ClientErrorException(credentialsResult.getStatus(), "no such credentials"));
break;
default:
result.fail(StatusCodeMapper.from(credentialsResult));
}
return result.future();
});
}
use of io.vertx.core.Future in project hono by eclipse.
the class CommandAndControlMqttIT method testSendCommandFailsForCommandNotAcknowledgedByDevice.
/**
* Verifies that the adapter forwards the <em>released</em> disposition back to the
* application if the device hasn't sent an acknowledgement for the command message
* published to the device.
*
* @param endpointConfig The endpoints to use for sending/receiving commands.
* @param ctx The vert.x test context.
* @throws InterruptedException if not all commands and responses are exchanged in time.
*/
@ParameterizedTest(name = IntegrationTestSupport.PARAMETERIZED_TEST_NAME_PATTERN)
@MethodSource("allCombinations")
@Timeout(timeUnit = TimeUnit.SECONDS, value = 20)
public void testSendCommandFailsForCommandNotAcknowledgedByDevice(final MqttCommandEndpointConfiguration endpointConfig, final VertxTestContext ctx) throws InterruptedException {
final MqttQoS subscribeQos = MqttQoS.AT_LEAST_ONCE;
final VertxTestContext setup = new VertxTestContext();
final Checkpoint ready = setup.checkpoint(2);
final String commandTargetDeviceId = endpointConfig.isSubscribeAsGateway() ? helper.setupGatewayDeviceBlocking(tenantId, deviceId, 5) : deviceId;
final int totalNoOfCommandsToSend = 2;
final CountDownLatch commandsFailed = new CountDownLatch(totalNoOfCommandsToSend);
final AtomicInteger receivedMessagesCounter = new AtomicInteger(0);
final AtomicInteger counter = new AtomicInteger();
final Handler<MqttPublishMessage> commandConsumer = msg -> {
LOGGER.trace("received command [{}] - no response sent here", msg.topicName());
final ResourceIdentifier topic = ResourceIdentifier.fromString(msg.topicName());
ctx.verify(() -> {
endpointConfig.assertCommandPublishTopicStructure(topic, commandTargetDeviceId, false, "setValue");
});
receivedMessagesCounter.incrementAndGet();
};
final Function<Buffer, Future<Void>> commandSender = payload -> {
counter.incrementAndGet();
return helper.sendCommand(tenantId, commandTargetDeviceId, "setValue", "text/plain", payload, helper.getSendCommandTimeout(counter.get() == 1)).mapEmpty();
};
helper.registry.addDeviceToTenant(tenantId, deviceId, password).compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password)).compose(ok -> injectMqttClientPubAckBlocker(new AtomicBoolean(true))).compose(ok -> createConsumer(tenantId, msg -> {
// expect empty notification with TTD -1
setup.verify(() -> assertThat(msg.getContentType()).isEqualTo(EventConstants.CONTENT_TYPE_EMPTY_NOTIFICATION));
final TimeUntilDisconnectNotification notification = msg.getTimeUntilDisconnectNotification().orElse(null);
LOGGER.info("received notification [{}]", notification);
setup.verify(() -> assertThat(notification).isNotNull());
if (notification.getTtd() == -1) {
ready.flag();
}
})).compose(conAck -> subscribeToCommands(commandTargetDeviceId, commandConsumer, endpointConfig, subscribeQos)).onComplete(setup.succeeding(ok -> ready.flag()));
assertWithMessage("setup of adapter finished within %s seconds", IntegrationTestSupport.getTestSetupTimeout()).that(setup.awaitCompletion(IntegrationTestSupport.getTestSetupTimeout(), TimeUnit.SECONDS)).isTrue();
if (setup.failed()) {
ctx.failNow(setup.causeOfFailure());
return;
}
final AtomicInteger commandsSent = new AtomicInteger(0);
final AtomicLong lastReceivedTimestamp = new AtomicLong(0);
final long start = System.currentTimeMillis();
while (commandsSent.get() < totalNoOfCommandsToSend) {
final CountDownLatch commandSent = new CountDownLatch(1);
context.runOnContext(go -> {
final Buffer msg = Buffer.buffer("value: " + commandsSent.getAndIncrement());
commandSender.apply(msg).onComplete(sendAttempt -> {
if (sendAttempt.succeeded()) {
LOGGER.debug("sending command {} succeeded unexpectedly", commandsSent.get());
} else {
if (sendAttempt.cause() instanceof ServerErrorException && ((ServerErrorException) sendAttempt.cause()).getErrorCode() == HttpURLConnection.HTTP_UNAVAILABLE && !(sendAttempt.cause() instanceof SendMessageTimeoutException)) {
LOGGER.debug("sending command {} failed as expected: {}", commandsSent.get(), sendAttempt.cause().toString());
lastReceivedTimestamp.set(System.currentTimeMillis());
commandsFailed.countDown();
if (commandsFailed.getCount() % 20 == 0) {
LOGGER.info("commands failed as expected: {}", totalNoOfCommandsToSend - commandsFailed.getCount());
}
} else {
LOGGER.debug("sending command {} failed with an unexpected error", commandsSent.get(), sendAttempt.cause());
}
}
commandSent.countDown();
});
});
commandSent.await();
}
// have to wait an extra MqttAdapterProperties.DEFAULT_COMMAND_ACK_TIMEOUT (100ms) for each command message
final long timeToWait = totalNoOfCommandsToSend * 300;
if (!commandsFailed.await(timeToWait, TimeUnit.MILLISECONDS)) {
LOGGER.info("Timeout of {} milliseconds reached, stop waiting for commands", timeToWait);
}
assertThat(receivedMessagesCounter.get()).isEqualTo(totalNoOfCommandsToSend);
final long commandsCompleted = totalNoOfCommandsToSend - commandsFailed.getCount();
LOGGER.info("commands sent: {}, commands failed: {} after {} milliseconds", commandsSent.get(), commandsCompleted, lastReceivedTimestamp.get() - start);
if (commandsCompleted == commandsSent.get()) {
ctx.completeNow();
} else {
ctx.failNow(new java.lang.IllegalStateException("did not complete all commands sent"));
}
}
Aggregations