use of org.eclipse.hono.application.client.DownstreamMessage 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 org.eclipse.hono.application.client.DownstreamMessage 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.DownstreamMessage in project hono by eclipse.
the class ProtonBasedApplicationClientTest method testCreateTelemetryConsumerReleasesMessageOnException.
/**
* Verifies that the message consumer created by the factory catches an exception
* thrown by the client provided handler and releases the message.
*
* @param ctx The vert.x test context.
*/
@Test
@SuppressWarnings("unchecked")
void testCreateTelemetryConsumerReleasesMessageOnException(final VertxTestContext ctx) {
// GIVEN a client provided message handler that throws an exception on
// each message received
final Handler<DownstreamMessage<AmqpMessageContext>> consumer = VertxMockSupport.mockHandler();
doThrow(new IllegalArgumentException("message does not contain required properties"), new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST)).when(consumer).handle(any(DownstreamMessage.class));
client.createTelemetryConsumer("tenant", consumer, t -> {
}).onComplete(ctx.succeeding(mc -> {
final ArgumentCaptor<ProtonMessageHandler> messageHandler = ArgumentCaptor.forClass(ProtonMessageHandler.class);
ctx.verify(() -> {
verify(connection).createReceiver(eq("telemetry/tenant"), eq(ProtonQoS.AT_LEAST_ONCE), messageHandler.capture(), anyInt(), anyBoolean(), VertxMockSupport.anyHandler());
final var msg = ProtonHelper.message();
// WHEN a message is received and the client provided consumer
// throws an IllegalArgumentException
var delivery = mock(ProtonDelivery.class);
messageHandler.getValue().handle(delivery, msg);
// THEN the message is forwarded to the client provided handler
verify(consumer).handle(any(DownstreamMessage.class));
// AND the AMQP message is being released
verify(delivery).disposition(any(Released.class), eq(Boolean.TRUE));
// WHEN a message is received and the client provided consumer
// throws a ClientErrorException
delivery = mock(ProtonDelivery.class);
messageHandler.getValue().handle(delivery, msg);
// THEN the message is forwarded to the client provided handler
verify(consumer, times(2)).handle(any(DownstreamMessage.class));
// AND the AMQP message is being rejected
verify(delivery).disposition(any(Rejected.class), eq(Boolean.TRUE));
});
ctx.completeNow();
}));
}
use of org.eclipse.hono.application.client.DownstreamMessage 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.DownstreamMessage 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