use of io.vertx.junit5.Timeout in project hono by eclipse.
the class CommandAndControlMqttIT method testSendCommandViaKafkaFailsForMalformedMessage.
/**
* Verifies that the adapter rejects malformed command messages sent by applications.
* <p>
* This test is applicable only if the messaging network type is Kafka.
*
* @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)
@AssumeMessagingSystem(type = MessagingType.kafka)
public void testSendCommandViaKafkaFailsForMalformedMessage(final MqttCommandEndpointConfiguration endpointConfig, final VertxTestContext ctx) throws InterruptedException {
final String commandTargetDeviceId = endpointConfig.isSubscribeAsGateway() ? helper.setupGatewayDeviceBlocking(tenantId, deviceId, 5) : deviceId;
final AtomicReference<GenericKafkaSender> kafkaSenderRef = new AtomicReference<>();
final CountDownLatch expectedCommandResponses = new CountDownLatch(1);
final VertxTestContext setup = new VertxTestContext();
final Checkpoint ready = setup.checkpoint(2);
final Future<MessageConsumer> kafkaAsyncErrorResponseConsumer = helper.createDeliveryFailureCommandResponseConsumer(ctx, tenantId, HttpURLConnection.HTTP_BAD_REQUEST, response -> expectedCommandResponses.countDown(), null);
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);
if (notification.getTtd() == -1) {
ready.flag();
}
}).compose(consumer -> helper.registry.addDeviceToTenant(tenantId, deviceId, password)).compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password)).compose(conAck -> subscribeToCommands(commandTargetDeviceId, msg -> {
// all commands should get rejected because they fail to pass the validity check
ctx.failNow(new IllegalStateException("should not have received command"));
}, endpointConfig, MqttQoS.AT_MOST_ONCE)).compose(ok -> helper.createGenericKafkaSender().onSuccess(kafkaSenderRef::set).mapEmpty()).compose(ok -> kafkaAsyncErrorResponseConsumer).onComplete(setup.succeeding(v -> 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 String commandTopic = new HonoTopic(HonoTopic.Type.COMMAND, tenantId).toString();
LOGGER.debug("sending command message lacking subject and correlation ID - no failure response expected here");
final Map<String, Object> properties1 = Map.of(MessageHelper.APP_PROPERTY_DEVICE_ID, deviceId, MessageHelper.SYS_PROPERTY_CONTENT_TYPE, MessageHelper.CONTENT_TYPE_OCTET_STREAM, KafkaRecordHelper.HEADER_RESPONSE_REQUIRED, true);
kafkaSenderRef.get().sendAndWaitForOutcome(commandTopic, tenantId, deviceId, Buffer.buffer(), properties1).onComplete(ctx.succeeding(ok -> {
}));
LOGGER.debug("sending command message lacking subject");
final String correlationId = "1";
final Map<String, Object> properties2 = Map.of(MessageHelper.SYS_PROPERTY_CORRELATION_ID, correlationId, MessageHelper.APP_PROPERTY_DEVICE_ID, deviceId, MessageHelper.SYS_PROPERTY_CONTENT_TYPE, MessageHelper.CONTENT_TYPE_OCTET_STREAM, KafkaRecordHelper.HEADER_RESPONSE_REQUIRED, true);
kafkaSenderRef.get().sendAndWaitForOutcome(commandTopic, tenantId, deviceId, Buffer.buffer(), properties2).onComplete(ctx.succeeding(ok -> {
}));
final long timeToWait = 2500;
if (!expectedCommandResponses.await(timeToWait, TimeUnit.MILLISECONDS)) {
LOGGER.info("Timeout of {} milliseconds reached, stop waiting for command response", timeToWait);
}
kafkaAsyncErrorResponseConsumer.result().close().onComplete(ar -> {
if (expectedCommandResponses.getCount() == 0) {
ctx.completeNow();
} else {
ctx.failNow(new java.lang.IllegalStateException("did not receive command response"));
}
});
}
use of io.vertx.junit5.Timeout in project hono by eclipse.
the class CommandAndControlMqttIT method testSendCommandSucceeds.
private void testSendCommandSucceeds(final VertxTestContext ctx, final String commandTargetDeviceId, final Handler<MqttPublishMessage> commandConsumer, final Function<Buffer, Future<Void>> commandSender, final MqttCommandEndpointConfiguration endpointConfig, final int totalNoOfCommandsToSend, final MqttQoS subscribeQos) throws InterruptedException {
final VertxTestContext setup = new VertxTestContext();
final Checkpoint ready = setup.checkpoint(2);
helper.registry.addDeviceToTenant(tenantId, deviceId, password).compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password)).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 Checkpoint sendCommandsSucceeded = ctx.checkpoint();
final CountDownLatch commandsSucceeded = new CountDownLatch(totalNoOfCommandsToSend);
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 -> {
commandsSent.getAndIncrement();
final Buffer msg = commandsSent.get() % 2 == 0 ? Buffer.buffer("value: " + commandsSent.get()) : // use 'null' payload for half the commands, ensuring such commands also get forwarded
null;
commandSender.apply(msg).onComplete(sendAttempt -> {
if (sendAttempt.failed()) {
LOGGER.info("error sending command {}", commandsSent.get(), sendAttempt.cause());
} else {
lastReceivedTimestamp.set(System.currentTimeMillis());
commandsSucceeded.countDown();
if (commandsSucceeded.getCount() % 20 == 0) {
LOGGER.info("commands succeeded: {}", totalNoOfCommandsToSend - commandsSucceeded.getCount());
}
}
if (commandsSent.get() % 20 == 0) {
LOGGER.info("commands sent: " + commandsSent.get());
}
commandSent.countDown();
});
});
commandSent.await();
}
final long timeToWait = totalNoOfCommandsToSend * 200;
if (!commandsSucceeded.await(timeToWait, TimeUnit.MILLISECONDS)) {
LOGGER.info("Timeout of {} milliseconds reached, stop waiting for commands to succeed", timeToWait);
}
if (lastReceivedTimestamp.get() == 0L) {
// no message has been received at all
lastReceivedTimestamp.set(System.currentTimeMillis());
}
final long commandsCompleted = totalNoOfCommandsToSend - commandsSucceeded.getCount();
LOGGER.info("commands sent: {}, commands succeeded: {} after {} milliseconds", commandsSent.get(), commandsCompleted, lastReceivedTimestamp.get() - start);
if (commandsCompleted == commandsSent.get()) {
sendCommandsSucceeded.flag();
} else {
ctx.failNow(new IllegalStateException("did not complete all commands sent"));
}
}
use of io.vertx.junit5.Timeout in project hono by eclipse.
the class CommandAndControlMqttIT method testSendOneWayCommandSucceeds.
/**
* Verifies that the adapter forwards on-way commands from
* an application to a 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 = 10)
public void testSendOneWayCommandSucceeds(final MqttCommandEndpointConfiguration endpointConfig, final VertxTestContext ctx) throws InterruptedException {
final String commandTargetDeviceId = endpointConfig.isSubscribeAsGateway() ? helper.setupGatewayDeviceBlocking(tenantId, deviceId, 5) : deviceId;
final Checkpoint commandsReceived = ctx.checkpoint(COMMANDS_TO_SEND);
final AtomicInteger counter = new AtomicInteger();
testSendCommandSucceeds(ctx, commandTargetDeviceId, msg -> {
LOGGER.trace("received one-way command [topic: {}]", msg.topicName());
final ResourceIdentifier topic = ResourceIdentifier.fromString(msg.topicName());
ctx.verify(() -> {
endpointConfig.assertCommandPublishTopicStructure(topic, commandTargetDeviceId, true, "setValue");
});
commandsReceived.flag();
}, payload -> {
counter.incrementAndGet();
return helper.sendOneWayCommand(tenantId, commandTargetDeviceId, "setValue", "text/plain", payload, helper.getSendCommandTimeout(counter.get() == 1));
}, endpointConfig, COMMANDS_TO_SEND, MqttQoS.AT_MOST_ONCE);
}
use of io.vertx.junit5.Timeout in project hono by eclipse.
the class HttpTestBase method testHandleConcurrentUploadWithTtd.
/**
* Verifies that for two consecutive upload requests containing a TTD, sent in close succession so that the command
* triggered by the first request isn't sent before the adapter has received the second upload request, the HTTP
* adapter returns the command as response to the second upload request.
*
* @param ctx The test context.
* @throws InterruptedException if the test is interrupted before having completed.
*/
@Test
@Timeout(timeUnit = TimeUnit.SECONDS, value = 20)
public void testHandleConcurrentUploadWithTtd(final VertxTestContext ctx) throws InterruptedException {
final Tenant tenant = new Tenant();
final CountDownLatch firstMessageReceived = new CountDownLatch(1);
final CountDownLatch secondMessageReceived = new CountDownLatch(1);
// GIVEN a registered device
final VertxTestContext setup = new VertxTestContext();
helper.registry.addDeviceForTenant(tenantId, tenant, deviceId, PWD).compose(ok -> createConsumer(tenantId, msg -> {
logger.trace("received message: {}", msg);
msg.getTimeUntilDisconnectNotification().ifPresent(notification -> {
logger.debug("processing piggy backed message [ttd: {}]", notification.getTtd());
ctx.verify(() -> {
assertThat(notification.getTenantId()).isEqualTo(tenantId);
assertThat(notification.getDeviceId()).isEqualTo(deviceId);
});
});
switch(msg.getContentType()) {
case "text/msg1":
logger.debug("received first message");
firstMessageReceived.countDown();
break;
case "text/msg2":
logger.debug("received second message");
secondMessageReceived.countDown();
break;
default:
}
})).compose(c -> {
// might fail immediately because the link has not been established yet.
return httpClient.create(getEndpointUri(), Buffer.buffer("trigger msg"), MultiMap.caseInsensitiveMultiMap().add(HttpHeaders.CONTENT_TYPE, "application/trigger").add(HttpHeaders.AUTHORIZATION, authorization).add(HttpHeaders.ORIGIN, ORIGIN_URI).add(Constants.HEADER_QOS_LEVEL, "1"), ResponsePredicate.status(200, 300));
}).onComplete(setup.succeedingThenComplete());
assertThat(setup.awaitCompletion(5, TimeUnit.SECONDS)).isTrue();
if (setup.failed()) {
ctx.failNow(setup.causeOfFailure());
return;
}
// WHEN the device sends a first upload request
MultiMap requestHeaders = MultiMap.caseInsensitiveMultiMap().add(HttpHeaders.CONTENT_TYPE, "text/msg1").add(HttpHeaders.AUTHORIZATION, authorization).add(HttpHeaders.ORIGIN, ORIGIN_URI).add(Constants.HEADER_TIME_TILL_DISCONNECT, "10");
final Future<HttpResponse<Buffer>> firstRequest = httpClient.create(getEndpointUri(), Buffer.buffer("hello one"), requestHeaders, ResponsePredicate.status(200, 300)).map(httpResponse -> {
logger.info("received response to first request");
return httpResponse;
});
logger.info("sent first request");
if (!firstMessageReceived.await(5, TimeUnit.SECONDS)) {
ctx.failNow(new IllegalStateException("first message not received in time"));
}
// followed by a second request
requestHeaders = MultiMap.caseInsensitiveMultiMap().add(HttpHeaders.CONTENT_TYPE, "text/msg2").add(HttpHeaders.AUTHORIZATION, authorization).add(HttpHeaders.ORIGIN, ORIGIN_URI).add(Constants.HEADER_TIME_TILL_DISCONNECT, "5");
final Future<HttpResponse<Buffer>> secondRequest = httpClient.create(getEndpointUri(), Buffer.buffer("hello two"), requestHeaders, ResponsePredicate.status(200, 300)).map(httpResponse -> {
logger.info("received response to second request");
return httpResponse;
});
logger.info("sent second request");
// wait for messages having been received
if (!secondMessageReceived.await(5, TimeUnit.SECONDS)) {
ctx.failNow(new IllegalStateException("second message not received in time"));
}
// send command
final JsonObject inputData = new JsonObject().put(COMMAND_JSON_KEY, (int) (Math.random() * 100));
final Future<Void> commandSent = helper.sendOneWayCommand(tenantId, deviceId, COMMAND_TO_SEND, "application/json", inputData.toBuffer(), 3000);
logger.info("sent one-way command to device");
// THEN both requests succeed
CompositeFuture.all(commandSent, firstRequest, secondRequest).onComplete(ctx.succeeding(ok -> {
ctx.verify(() -> {
// and the response to the second request contains a command
assertThat(secondRequest.result().getHeader(Constants.HEADER_COMMAND)).isEqualTo(COMMAND_TO_SEND);
// while the response to the first request is empty
assertThat(firstRequest.result().getHeader(Constants.HEADER_COMMAND)).isNull();
});
ctx.completeNow();
}));
}
use of io.vertx.junit5.Timeout in project hono by eclipse.
the class KafkaBasedCommandConsumerFactoryImplIT method testCommandsGetForwardedIfOneConsumerInstanceGetsClosed.
/**
* Verifies that records, published on the tenant-specific Kafka command topic, get received
* and forwarded by consumers created by factory instances even if one factory and its contained
* consumer gets closed in the middle of processing some of the commands.
*
* @param ctx The vert.x test context.
* @throws InterruptedException if test execution gets interrupted.
*/
@Test
@Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
public void testCommandsGetForwardedIfOneConsumerInstanceGetsClosed(final VertxTestContext ctx) throws InterruptedException {
final String tenantId = "tenant_" + UUID.randomUUID();
final VertxTestContext setup = new VertxTestContext();
// Scenario to test:
// - first command gets sent, forwarded and received without any imposed delay
// - second command gets sent, received by the factory consumer instance; processing gets blocked
// while trying to get the target adapter instance
// - for the rest of the commands, retrieval of the target adapter instance is successful, but they won't
// get forwarded until processing of the second command is finished
// - now the factory consumer gets closed and a new factory/consumer gets started; at that point
// also the processing of the second command gets finished
//
// Expected outcome:
// - processing of the second command and all following commands by the first consumer gets aborted, so that
// these commands don't get forwarded on the internal command topic
// - instead, the second consumer takes over at the offset of the first command (position must have been committed
// when closing the first consumer) and processes and forwards all commands starting with the second command
final int numTestCommands = 10;
final List<KafkaConsumerRecord<String, Buffer>> receivedRecords = new ArrayList<>();
final Promise<Void> firstRecordReceivedPromise = Promise.promise();
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() == 1) {
firstRecordReceivedPromise.complete();
}
if (receivedRecords.size() == numTestCommands) {
allRecordsReceivedPromise.tryComplete();
}
};
final Promise<Void> firstConsumerAllGetAdapterInstanceInvocationsDone = Promise.promise();
final LinkedList<Promise<Void>> firstConsumerGetAdapterInstancePromisesQueue = new LinkedList<>();
// don't let getting the target adapter instance finish immediately
final Supplier<Future<Void>> firstConsumerGetAdapterInstanceSupplier = () -> {
final Promise<Void> resultPromise = Promise.promise();
firstConsumerGetAdapterInstancePromisesQueue.addFirst(resultPromise);
// don't complete the future for the second command here yet
if (firstConsumerGetAdapterInstancePromisesQueue.size() != 2) {
resultPromise.complete();
}
if (firstConsumerGetAdapterInstancePromisesQueue.size() == numTestCommands) {
firstConsumerAllGetAdapterInstanceInvocationsDone.complete();
}
return resultPromise.future();
};
final AtomicReference<KafkaBasedCommandConsumerFactoryImpl> consumerFactory1Ref = new AtomicReference<>();
final Context vertxContext = vertx.getOrCreateContext();
vertxContext.runOnContext(v0 -> {
final HonoKafkaConsumer internalConsumer = getInternalCommandConsumer(recordHandler);
final KafkaBasedCommandConsumerFactoryImpl consumerFactory1 = getKafkaBasedCommandConsumerFactory(firstConsumerGetAdapterInstanceSupplier, tenantId);
consumerFactory1Ref.set(consumerFactory1);
CompositeFuture.join(internalConsumer.start(), consumerFactory1.start()).compose(f -> createCommandConsumer(tenantId, consumerFactory1)).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 AtomicInteger secondConsumerGetAdapterInstanceInvocations = new AtomicInteger();
// wait for first record on internal topic to have been received ...
CompositeFuture.join(firstConsumerAllGetAdapterInstanceInvocationsDone.future(), firstRecordReceivedPromise.future()).compose(v -> {
// ... and wait some more, making sure that the offset of the first record has been committed
final Promise<Void> delayPromise = Promise.promise();
vertx.setTimer(500, tid -> delayPromise.complete());
return delayPromise.future();
}).onComplete(v -> {
LOG.info("stopping first consumer factory");
consumerFactory1Ref.get().stop().onComplete(ctx.succeeding(ar -> {
LOG.info("factory stopped");
// no delay on getting the target adapter instance added here
final KafkaBasedCommandConsumerFactoryImpl consumerFactory2 = getKafkaBasedCommandConsumerFactory(() -> {
secondConsumerGetAdapterInstanceInvocations.incrementAndGet();
return Future.succeededFuture();
}, tenantId);
consumerFactory2.start().onComplete(ctx.succeeding(ar2 -> {
LOG.info("creating command consumer in new consumer factory");
createCommandConsumer(tenantId, consumerFactory2).onComplete(ctx.succeeding(ar3 -> {
LOG.debug("consumer created");
firstConsumerGetAdapterInstancePromisesQueue.forEach(Promise::tryComplete);
}));
}));
}));
});
final long timerId = vertx.setTimer(8000, tid -> {
LOG.info("received records:\n{}", receivedRecords.stream().map(Object::toString).collect(Collectors.joining(",\n")));
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);
// all but the first command should have been processed by the second consumer
assertThat(secondConsumerGetAdapterInstanceInvocations.get()).isEqualTo(numTestCommands - 1);
});
ctx.completeNow();
}));
}
Aggregations