use of org.eclipse.hono.application.client.kafka.KafkaMessageContext in project hono by eclipse.
the class KafkaBasedCommandSenderTest method sendCommandAndReceiveResponse.
private void sendCommandAndReceiveResponse(final VertxTestContext ctx, final String correlationId, final Integer responseStatus, final String responsePayload, final boolean expectSuccess, final int expectedStatusCode) {
final Context context = vertx.getOrCreateContext();
final Promise<Void> onProducerRecordSentPromise = Promise.promise();
mockProducer = new MockProducer<>(true, new StringSerializer(), new BufferSerializer()) {
@Override
public synchronized java.util.concurrent.Future<RecordMetadata> send(final ProducerRecord<String, Buffer> record, final Callback callback) {
return super.send(record, (metadata, exception) -> {
callback.onCompletion(metadata, exception);
context.runOnContext(v -> {
// decouple from current execution in order to run after the "send" result handler
onProducerRecordSentPromise.complete();
});
});
}
};
final var producerFactory = CachingKafkaProducerFactory.testFactory(vertx, (n, c) -> KafkaClientUnitTestHelper.newKafkaProducer(mockProducer));
commandSender = new KafkaBasedCommandSender(vertx, consumerConfig, producerFactory, producerConfig, NoopTracerFactory.create());
final Map<String, Object> headerProperties = new HashMap<>();
headerProperties.put("appKey", "appValue");
final String command = "setVolume";
final ConsumerRecord<String, Buffer> commandResponseRecord = commandResponseRecord(tenantId, deviceId, correlationId, responseStatus, Buffer.buffer(responsePayload));
final String responseTopic = new HonoTopic(HonoTopic.Type.COMMAND_RESPONSE, tenantId).toString();
final TopicPartition responseTopicPartition = new TopicPartition(responseTopic, 0);
mockConsumer.setRebalancePartitionAssignmentAfterSubscribe(List.of(responseTopicPartition));
mockConsumer.updatePartitions(responseTopicPartition, KafkaMockConsumer.DEFAULT_NODE);
mockConsumer.updateBeginningOffsets(Map.of(responseTopicPartition, 0L));
mockConsumer.updateEndOffsets(Map.of(responseTopicPartition, 0L));
onProducerRecordSentPromise.future().onComplete(ar -> {
LOG.debug("producer record sent, add command response record to mockConsumer");
// Send a command response with the same correlation id as that of the command
mockConsumer.addRecord(commandResponseRecord);
});
// This correlation id is used for both command and its response.
commandSender.setCorrelationIdSupplier(() -> correlationId);
commandSender.setKafkaConsumerSupplier(() -> mockConsumer);
context.runOnContext(v -> {
// Send a command to the device
commandSender.sendCommand(tenantId, deviceId, command, "text/plain", Buffer.buffer("test"), headerProperties).onComplete(ar -> {
ctx.verify(() -> {
if (expectSuccess) {
// assert that send operation succeeded
assertThat(ar.succeeded()).isTrue();
// Verify the command response that has been received
final DownstreamMessage<KafkaMessageContext> response = ar.result();
assertThat(response.getDeviceId()).isEqualTo(deviceId);
assertThat(response.getStatus()).isEqualTo(responseStatus);
assertThat(response.getPayload().toString()).isEqualTo(responsePayload);
} else {
// assert that send operation failed
assertThat(ar.succeeded()).isFalse();
assertThat(ar.cause()).isInstanceOf(ServiceInvocationException.class);
assertThat(((ServiceInvocationException) ar.cause()).getErrorCode()).isEqualTo(expectedStatusCode);
assertThat(ar.cause().getMessage()).isEqualTo(responsePayload);
}
});
ctx.completeNow();
mockConsumer.close();
commandSender.stop();
});
});
}
use of org.eclipse.hono.application.client.kafka.KafkaMessageContext in project hono by eclipse.
the class KafkaBasedCommandSender method sendCommand.
/**
* {@inheritDoc}
*
* <p>
* The replyId is not used in the Kafka based implementation. It can be set to {@code null}.
* If set it will be ignored.
* <p>
* If the timeout duration is {@code null} then the default timeout value of
* {@value DEFAULT_COMMAND_TIMEOUT_IN_MS} ms is used.
*/
@Override
public Future<DownstreamMessage<KafkaMessageContext>> 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 String correlationId = correlationIdSupplier.get();
final Span span = TracingHelper.buildChildSpan(tracer, context, "send command and receive response", getClass().getSimpleName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, tenantId).withTag(TracingHelper.TAG_DEVICE_ID, deviceId).withTag(TracingHelper.TAG_CORRELATION_ID, correlationId).start();
final ExpiringCommandPromise expiringCommandPromise = new ExpiringCommandPromise(correlationId, timeoutInMs, // Remove the corresponding pending response entry if times out
x -> removePendingCommandResponse(tenantId, correlationId), span);
subscribeForCommandResponse(tenantId, span).compose(ok -> {
// Store the correlation id and the expiring command promise
pendingCommandResponses.computeIfAbsent(tenantId, k -> new ConcurrentHashMap<>()).put(correlationId, expiringCommandPromise);
return sendCommand(tenantId, deviceId, command, contentType, data, correlationId, properties, true, "send command", span.context()).onSuccess(sent -> {
LOGGER.debug("sent command [correlation-id: {}], waiting for response", correlationId);
span.log("sent command, waiting for response");
}).onFailure(error -> {
LOGGER.debug("error sending command", error);
// To ensure that the span is not already finished.
if (!expiringCommandPromise.future().isComplete()) {
TracingHelper.logError(span, "error sending command", error);
}
removePendingCommandResponse(tenantId, correlationId);
expiringCommandPromise.tryCompleteAndCancelTimer(Future.failedFuture(error));
});
});
return expiringCommandPromise.future().onComplete(o -> span.finish());
}
use of org.eclipse.hono.application.client.kafka.KafkaMessageContext in project hono by eclipse.
the class KafkaApplicationClientImpl method createKafkaBasedDownstreamMessageConsumer.
private Future<MessageConsumer> createKafkaBasedDownstreamMessageConsumer(final String tenantId, final HonoTopic.Type type, final Handler<DownstreamMessage<KafkaMessageContext>> messageHandler) {
Objects.requireNonNull(tenantId);
Objects.requireNonNull(type);
Objects.requireNonNull(messageHandler);
final String topic = new HonoTopic(type, tenantId).toString();
final Handler<KafkaConsumerRecord<String, Buffer>> recordHandler = record -> {
messageHandler.handle(new KafkaDownstreamMessage(record));
};
final HonoKafkaConsumer consumer = new HonoKafkaConsumer(vertx, Set.of(topic), recordHandler, consumerConfig.getConsumerConfig(type.toString()));
consumer.setPollTimeout(Duration.ofMillis(consumerConfig.getPollTimeout()));
Optional.ofNullable(kafkaConsumerSupplier).ifPresent(consumer::setKafkaConsumerSupplier);
return consumer.start().map(v -> (MessageConsumer) new MessageConsumer() {
@Override
public Future<Void> close() {
return consumer.stop();
}
}).onSuccess(consumersToCloseOnStop::add);
}
Aggregations