use of org.eclipse.hono.client.kafka.consumer.HonoKafkaConsumer in project hono by eclipse.
the class HonoKafkaConsumerIT method testConsumerReadsLatestRecordsPublishedAfterStart.
/**
* Verifies that a HonoKafkaConsumer configured with "latest" as offset reset strategy only receives
* records published after the consumer <em>start()</em> method has completed.
*
* @param partitionAssignmentStrategy The partition assignment strategy to use for the consumer.
* @param ctx The vert.x test context.
* @throws InterruptedException if test execution gets interrupted.
*/
@ParameterizedTest
@MethodSource("partitionAssignmentStrategies")
@Timeout(value = 10, timeUnit = TimeUnit.SECONDS)
public void testConsumerReadsLatestRecordsPublishedAfterStart(final String partitionAssignmentStrategy, final VertxTestContext ctx) throws InterruptedException {
final int numTopics = 2;
final int numPartitions = 5;
final int numTestRecordsPerTopic = 20;
final Set<String> topics = IntStream.range(0, numTopics).mapToObj(i -> "test_" + i + "_" + UUID.randomUUID()).collect(Collectors.toSet());
final String publishTestTopic = topics.iterator().next();
final VertxTestContext setup = new VertxTestContext();
createTopics(topics, numPartitions).compose(v -> publishRecords(numTestRecordsPerTopic, "key_", topics)).onComplete(setup.succeedingThenComplete());
assertThat(setup.awaitCompletion(IntegrationTestSupport.getTestSetupTimeout(), TimeUnit.SECONDS)).isTrue();
if (setup.failed()) {
ctx.failNow(setup.causeOfFailure());
return;
}
LOG.debug("topics created and (to be ignored) test records published");
// prepare consumer
final Map<String, String> consumerConfig = IntegrationTestSupport.getKafkaConsumerConfig().getConsumerConfig("test");
applyPartitionAssignmentStrategy(consumerConfig, partitionAssignmentStrategy);
consumerConfig.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
final String publishedAfterStartRecordKey = "publishedAfterStartKey";
final Handler<KafkaConsumerRecord<String, Buffer>> recordHandler = record -> {
// verify received record
ctx.verify(() -> assertThat(record.key()).isEqualTo(publishedAfterStartRecordKey));
ctx.completeNow();
};
kafkaConsumer = new HonoKafkaConsumer(vertx, topics, recordHandler, consumerConfig);
// start consumer
kafkaConsumer.start().onComplete(ctx.succeeding(v -> {
LOG.debug("consumer started, publish record to be received by the consumer");
publish(publishTestTopic, publishedAfterStartRecordKey, Buffer.buffer("testPayload"));
}));
if (!ctx.awaitCompletion(9, TimeUnit.SECONDS)) {
ctx.failNow(new IllegalStateException("timeout waiting for record to be received"));
}
}
use of org.eclipse.hono.client.kafka.consumer.HonoKafkaConsumer 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