use of org.eclipse.hono.client.command.CommandResponse in project hono by eclipse.
the class AbstractVertxBasedMqttProtocolAdapter method uploadCommandResponseMessage.
/**
* Uploads a command response message.
*
* @param ctx The context in which the MQTT message has been published.
* @param targetAddress The address that the response should be forwarded to.
* @return A future indicating the outcome of the operation.
* <p>
* The future will succeed if the message has been uploaded successfully.
* Otherwise, the future will fail with a {@link ServiceInvocationException}.
* @throws NullPointerException if any of the parameters are {@code null}.
*/
public final Future<Void> uploadCommandResponseMessage(final MqttContext ctx, final ResourceIdentifier targetAddress) {
Objects.requireNonNull(ctx);
Objects.requireNonNull(targetAddress);
final String[] addressPath = targetAddress.getResourcePath();
Integer status = null;
String reqId = null;
final Future<CommandResponse> commandResponseTracker;
if (addressPath.length <= CommandConstants.TOPIC_POSITION_RESPONSE_STATUS) {
commandResponseTracker = Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "command response topic has too few segments"));
} else {
try {
status = Integer.parseInt(addressPath[CommandConstants.TOPIC_POSITION_RESPONSE_STATUS]);
} catch (final NumberFormatException e) {
log.trace("got invalid status code [{}] [tenant-id: {}, device-id: {}]", addressPath[CommandConstants.TOPIC_POSITION_RESPONSE_STATUS], targetAddress.getTenantId(), targetAddress.getResourceId());
}
if (status != null) {
reqId = addressPath[CommandConstants.TOPIC_POSITION_RESPONSE_REQ_ID];
final CommandResponse commandResponse = CommandResponse.fromRequestId(reqId, targetAddress.getTenantId(), targetAddress.getResourceId(), ctx.message().payload(), ctx.contentType(), status);
commandResponseTracker = commandResponse != null ? Future.succeededFuture(commandResponse) : Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "command response topic contains invalid data"));
} else {
// status code could not be parsed
commandResponseTracker = Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "invalid status code"));
}
}
final Span currentSpan = TracingHelper.buildChildSpan(tracer, ctx.getTracingContext(), "upload Command response", getTypeName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, targetAddress.getTenantId()).withTag(TracingHelper.TAG_DEVICE_ID, targetAddress.getResourceId()).withTag(Constants.HEADER_COMMAND_RESPONSE_STATUS, status).withTag(Constants.HEADER_COMMAND_REQUEST_ID, reqId).withTag(TracingHelper.TAG_AUTHENTICATED.getKey(), ctx.authenticatedDevice() != null).start();
final int payloadSize = Optional.ofNullable(ctx.message().payload()).map(Buffer::length).orElse(0);
final Future<TenantObject> tenantTracker = getTenantConfiguration(targetAddress.getTenantId(), ctx.getTracingContext());
return CompositeFuture.all(tenantTracker, commandResponseTracker).compose(success -> {
final Future<RegistrationAssertion> deviceRegistrationTracker = getRegistrationAssertion(targetAddress.getTenantId(), targetAddress.getResourceId(), ctx.authenticatedDevice(), currentSpan.context());
final Future<Void> tenantValidationTracker = CompositeFuture.all(isAdapterEnabled(tenantTracker.result()), checkMessageLimit(tenantTracker.result(), payloadSize, currentSpan.context())).mapEmpty();
return CompositeFuture.all(deviceRegistrationTracker, tenantValidationTracker).compose(ok -> sendCommandResponse(tenantTracker.result(), deviceRegistrationTracker.result(), commandResponseTracker.result(), currentSpan.context()));
}).compose(delivery -> {
log.trace("successfully forwarded command response from device [tenant-id: {}, device-id: {}]", targetAddress.getTenantId(), targetAddress.getResourceId());
metrics.reportCommand(Direction.RESPONSE, targetAddress.getTenantId(), tenantTracker.result(), ProcessingOutcome.FORWARDED, payloadSize, ctx.getTimer());
// check that the remote MQTT client is still connected before sending PUBACK
if (ctx.isAtLeastOnce() && ctx.deviceEndpoint().isConnected()) {
currentSpan.log(EVENT_SENDING_PUBACK);
ctx.acknowledge();
}
currentSpan.finish();
return Future.<Void>succeededFuture();
}).recover(t -> {
TracingHelper.logError(currentSpan, t);
currentSpan.finish();
metrics.reportCommand(Direction.RESPONSE, targetAddress.getTenantId(), tenantTracker.result(), ProcessingOutcome.from(t), payloadSize, ctx.getTimer());
return Future.failedFuture(t);
});
}
use of org.eclipse.hono.client.command.CommandResponse in project hono by eclipse.
the class KafkaBasedCommandResponseSenderTest method testIfValidCommandResponseKafkaRecordIsSent.
@Test
void testIfValidCommandResponseKafkaRecordIsSent(final VertxTestContext ctx, final Vertx vertx) {
// GIVEN a command response sender
final String tenantId = "test-tenant";
final String deviceId = "test-device";
final String correlationId = UUID.randomUUID().toString();
final String contentType = "text/plain";
final String payload = "the-payload";
final int status = 200;
final String additionalHeader1Name = "testHeader1";
final String additionalHeader1Value = "testHeader1Value";
final String additionalHeader2Name = "testHeader2";
final String additionalHeader2Value = "testHeader2Value";
final Map<String, Object> additionalProperties = Map.of(additionalHeader1Name, additionalHeader1Value, additionalHeader2Name, additionalHeader2Value);
final CommandResponse commandResponse = CommandResponse.fromAddressAndCorrelationId(String.format("%s/%s/%s", CommandConstants.COMMAND_RESPONSE_ENDPOINT, tenantId, Commands.getDeviceFacingReplyToId("", deviceId, MessagingType.kafka)), correlationId, Buffer.buffer(payload), contentType, status);
commandResponse.setAdditionalProperties(additionalProperties);
final Span span = TracingMockSupport.mockSpan();
final Tracer tracer = TracingMockSupport.mockTracer(span);
final var mockProducer = KafkaClientUnitTestHelper.newMockProducer(true);
final var factory = CachingKafkaProducerFactory.testFactory(vertx, (n, c) -> KafkaClientUnitTestHelper.newKafkaProducer(mockProducer));
final var sender = new KafkaBasedCommandResponseSender(vertx, factory, kafkaProducerConfig, tracer);
final TenantObject tenant = TenantObject.from(tenantId);
tenant.setResourceLimits(new ResourceLimits().setMaxTtlCommandResponse(10L));
// WHEN sending a command response
sender.sendCommandResponse(tenant, new RegistrationAssertion(deviceId), commandResponse, NoopSpan.INSTANCE.context()).onComplete(ctx.succeeding(t -> {
ctx.verify(() -> {
// THEN the producer record is created from the given values...
final ProducerRecord<String, Buffer> record = mockProducer.history().get(0);
assertThat(record.key()).isEqualTo(deviceId);
assertThat(record.topic()).isEqualTo(new HonoTopic(HonoTopic.Type.COMMAND_RESPONSE, tenantId).toString());
assertThat(record.value().toString()).isEqualTo(payload);
// Verify if the record contains the necessary headers.
final Headers headers = record.headers();
KafkaClientUnitTestHelper.assertUniqueHeaderWithExpectedValue(headers, MessageHelper.APP_PROPERTY_TENANT_ID, tenantId);
KafkaClientUnitTestHelper.assertUniqueHeaderWithExpectedValue(headers, MessageHelper.APP_PROPERTY_DEVICE_ID, deviceId);
KafkaClientUnitTestHelper.assertUniqueHeaderWithExpectedValue(headers, MessageHelper.SYS_PROPERTY_CORRELATION_ID, correlationId);
KafkaClientUnitTestHelper.assertUniqueHeaderWithExpectedValue(headers, MessageHelper.APP_PROPERTY_STATUS, status);
KafkaClientUnitTestHelper.assertUniqueHeaderWithExpectedValue(headers, MessageHelper.SYS_PROPERTY_CONTENT_TYPE, contentType);
final var creationTimeHeader = headers.headers(MessageHelper.SYS_PROPERTY_CREATION_TIME);
assertThat(creationTimeHeader).hasSize(1);
final Long creationTimeMillis = Json.decodeValue(Buffer.buffer(creationTimeHeader.iterator().next().value()), Long.class);
assertThat(creationTimeMillis).isGreaterThan(0L);
KafkaClientUnitTestHelper.assertUniqueHeaderWithExpectedValue(headers, MessageHelper.SYS_HEADER_PROPERTY_TTL, 10000L);
KafkaClientUnitTestHelper.assertUniqueHeaderWithExpectedValue(headers, additionalHeader1Name, additionalHeader1Value);
KafkaClientUnitTestHelper.assertUniqueHeaderWithExpectedValue(headers, additionalHeader2Name, additionalHeader2Value);
verify(span).finish();
});
ctx.completeNow();
}));
}
use of org.eclipse.hono.client.command.CommandResponse in project hono by eclipse.
the class KafkaBasedCommandContext method sendDeliveryFailureCommandResponseMessage.
private Future<Void> sendDeliveryFailureCommandResponseMessage(final int status, final String error, final Span span, final Throwable cause) {
final JsonObject payloadJson = new JsonObject();
payloadJson.put("error", error != null ? error : "");
final String correlationId = getCorrelationId();
if (correlationId == null) {
TracingHelper.logError(span, "can't send command response message - no correlation id set");
return Future.failedFuture("missing correlation id");
}
final CommandResponse commandResponse = new CommandResponse(command.getTenant(), command.getDeviceId(), payloadJson.toBuffer(), CommandConstants.CONTENT_TYPE_DELIVERY_FAILURE_NOTIFICATION, status, correlationId, "", MessagingType.kafka);
commandResponse.setAdditionalProperties(Collections.unmodifiableMap(command.getDeliveryFailureNotificationProperties()));
return commandResponseSender.sendCommandResponse(// try to retrieve tenant configuration from context
Optional.ofNullable(get(KEY_TENANT_CONFIG)).filter(TenantObject.class::isInstance).map(TenantObject.class::cast).orElseGet(() -> TenantObject.from(command.getTenant())), new RegistrationAssertion(command.getDeviceId()), commandResponse, span.context()).onFailure(thr -> {
LOG.debug("failed to publish command response [{}]", commandResponse, thr);
TracingHelper.logError(span, "failed to publish command response message", thr);
}).onSuccess(v -> {
LOG.debug("published error command response [{}, cause: {}]", commandResponse, cause != null ? cause.getMessage() : error);
span.log("published error command response");
});
}
use of org.eclipse.hono.client.command.CommandResponse in project hono by eclipse.
the class ProtonBasedCommandResponseSenderTest method testCommandResponseMessageHasCreationTimeAndTtl.
/**
* Verifies that command response messages being sent downstream contain a creation-time
* and a time-to-live as defined at the tenant level.
*/
@Test
public void testCommandResponseMessageHasCreationTimeAndTtl() {
final var now = Instant.now();
final TenantObject tenant = TenantObject.from(TENANT_ID);
tenant.setResourceLimits(new ResourceLimits().setMaxTtlCommandResponse(10L));
when(protonSender.sendQueueFull()).thenReturn(Boolean.FALSE);
when(protonSender.send(any(Message.class), VertxMockSupport.anyHandler())).thenReturn(mock(ProtonDelivery.class));
// WHEN sending a command response message
final CommandResponse commandResponse = CommandResponse.fromRequestId(Commands.encodeRequestIdParameters(CORRELATION_ID, REPLY_TO_ID, DEVICE_ID, MessagingType.amqp), TENANT_ID, DEVICE_ID, null, null, HttpURLConnection.HTTP_OK);
sender.sendCommandResponse(tenant, new RegistrationAssertion(DEVICE_ID), commandResponse, span.context());
final ArgumentCaptor<Message> downstreamMessage = ArgumentCaptor.forClass(Message.class);
verify(protonSender).send(downstreamMessage.capture(), VertxMockSupport.anyHandler());
// THEN the message being sent contains a creation-time
assertThat(downstreamMessage.getValue().getCreationTime()).isAtLeast(now.toEpochMilli());
// and a TTL
assertThat(downstreamMessage.getValue().getTtl()).isEqualTo(10_000L);
// and a 200 status code
assertThat(MessageHelper.getStatus(downstreamMessage.getValue())).isEqualTo(HttpURLConnection.HTTP_OK);
}
use of org.eclipse.hono.client.command.CommandResponse in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapter method doUploadCommandResponseMessage.
private Future<Void> doUploadCommandResponseMessage(final AmqpContext context, final ResourceIdentifier resource, final Span currentSpan) {
final Future<CommandResponse> responseTracker = Optional.ofNullable(getCommandResponse(context.getMessage())).map(Future::succeededFuture).orElseGet(() -> {
TracingHelper.logError(currentSpan, String.format("invalid message (correlationId: %s, address: %s, status: %s)", context.getMessage().getCorrelationId(), context.getMessage().getAddress(), MessageHelper.getStatus(context.getMessage())));
return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "malformed command response message"));
});
final Future<TenantObject> tenantTracker = getTenantConfiguration(resource.getTenantId(), currentSpan.context());
return CompositeFuture.all(tenantTracker, responseTracker).compose(ok -> {
final CommandResponse commandResponse = responseTracker.result();
log.trace("sending command response [device-id: {}, status: {}, correlation-id: {}, reply-to: {}]", resource.getResourceId(), commandResponse.getStatus(), commandResponse.getCorrelationId(), commandResponse.getReplyToId());
final Map<String, Object> items = new HashMap<>(3);
items.put(Fields.EVENT, "sending command response");
items.put(TracingHelper.TAG_CORRELATION_ID.getKey(), commandResponse.getCorrelationId());
items.put(MessageHelper.APP_PROPERTY_STATUS, commandResponse.getStatus());
currentSpan.log(items);
final Future<RegistrationAssertion> tokenFuture = getRegistrationAssertion(resource.getTenantId(), resource.getResourceId(), context.getAuthenticatedDevice(), currentSpan.context());
final Future<TenantObject> tenantValidationTracker = CompositeFuture.all(isAdapterEnabled(tenantTracker.result()), checkMessageLimit(tenantTracker.result(), context.getPayloadSize(), currentSpan.context())).map(success -> tenantTracker.result());
return CompositeFuture.all(tenantValidationTracker, tokenFuture).compose(success -> sendCommandResponse(tenantTracker.result(), tokenFuture.result(), commandResponse, currentSpan.context()));
}).map(delivery -> {
log.trace("forwarded command response from device [tenant: {}, device-id: {}]", resource.getTenantId(), resource.getResourceId());
metrics.reportCommand(Direction.RESPONSE, resource.getTenantId(), tenantTracker.result(), ProcessingOutcome.FORWARDED, context.getPayloadSize(), context.getTimer());
return delivery;
}).recover(t -> {
log.debug("cannot process command response from device [tenant: {}, device-id: {}]", resource.getTenantId(), resource.getResourceId(), t);
metrics.reportCommand(Direction.RESPONSE, resource.getTenantId(), tenantTracker.result(), ProcessingOutcome.from(t), context.getPayloadSize(), context.getTimer());
return Future.failedFuture(t);
});
}
Aggregations