use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class AbstractHonoResource method createCommandConsumer.
/**
* Creates a consumer for command messages to be sent to a device.
*
* @param ttdSecs The number of seconds the device waits for a command.
* @param tenantObject The tenant configuration object.
* @param deviceId The identifier of the device.
* @param gatewayId The identifier of the gateway that is acting on behalf of the device or {@code null} otherwise.
* @param context The device's currently executing CoAP request context.
* @param responseReady A future to complete once one of the following conditions are met:
* <ul>
* <li>the request did not include a <em>hono-ttd</em> query-parameter or</li>
* <li>a command has been received and the response ready future has not yet been completed or</li>
* <li>the ttd has expired</li>
* </ul>
* @param uploadMessageSpan The OpenTracing Span used for tracking the processing of the request.
* @return A future indicating the outcome of the operation.
* <p>
* The future will be completed with the created message consumer or {@code null}, if the response can be
* sent back to the device without waiting for a command.
* <p>
* The future will be failed with a {@code ServiceInvocationException} if the message consumer could not be
* created.
* @throws NullPointerException if any of the parameters other than TTD or gatewayId is {@code null}.
*/
protected final Future<CommandConsumer> createCommandConsumer(final Integer ttdSecs, final TenantObject tenantObject, final String deviceId, final String gatewayId, final CoapContext context, final Handler<AsyncResult<Void>> responseReady, final Span uploadMessageSpan) {
Objects.requireNonNull(tenantObject);
Objects.requireNonNull(deviceId);
Objects.requireNonNull(context);
Objects.requireNonNull(responseReady);
Objects.requireNonNull(uploadMessageSpan);
if (ttdSecs == null || ttdSecs <= 0) {
// no need to wait for a command
responseReady.handle(Future.succeededFuture());
return Future.succeededFuture();
}
final AtomicBoolean requestProcessed = new AtomicBoolean(false);
uploadMessageSpan.setTag(MessageHelper.APP_PROPERTY_DEVICE_TTD, ttdSecs);
final Span waitForCommandSpan = TracingHelper.buildChildSpan(getTracer(), uploadMessageSpan.context(), "create consumer and wait for command", getAdapter().getTypeName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, tenantObject.getTenantId()).withTag(TracingHelper.TAG_DEVICE_ID, deviceId).start();
final Handler<CommandContext> commandHandler = commandContext -> {
final Span processCommandSpan = TracingHelper.buildFollowsFromSpan(getTracer(), waitForCommandSpan.context(), "process received command").withTag(Tags.COMPONENT.getKey(), getAdapter().getTypeName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, tenantObject.getTenantId()).withTag(TracingHelper.TAG_DEVICE_ID, deviceId).addReference(References.FOLLOWS_FROM, commandContext.getTracingContext()).start();
Tags.COMPONENT.set(commandContext.getTracingSpan(), getAdapter().getTypeName());
commandContext.logCommandToSpan(processCommandSpan);
final Command command = commandContext.getCommand();
final Sample commandSample = getAdapter().getMetrics().startTimer();
if (isCommandValid(command, processCommandSpan)) {
if (requestProcessed.compareAndSet(false, true)) {
waitForCommandSpan.finish();
getAdapter().checkMessageLimit(tenantObject, command.getPayloadSize(), processCommandSpan.context()).onComplete(result -> {
if (result.succeeded()) {
addMicrometerSample(commandContext, commandSample);
// put command context to routing context and notify
context.put(CommandContext.KEY_COMMAND_CONTEXT, commandContext);
} else {
commandContext.reject(result.cause());
TracingHelper.logError(processCommandSpan, "rejected command for device", result.cause());
getAdapter().getMetrics().reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenantObject.getTenantId(), tenantObject, ProcessingOutcome.from(result.cause()), command.getPayloadSize(), commandSample);
}
cancelCommandReceptionTimer(context);
setTtdStatus(context, TtdStatus.COMMAND);
responseReady.handle(Future.succeededFuture());
processCommandSpan.finish();
});
} else {
final String errorMsg = "waiting time for command has elapsed or another command has already been processed";
LOG.debug("{} [tenantId: {}, deviceId: {}]", errorMsg, tenantObject.getTenantId(), deviceId);
getAdapter().getMetrics().reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenantObject.getTenantId(), tenantObject, ProcessingOutcome.UNDELIVERABLE, command.getPayloadSize(), commandSample);
commandContext.release(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, errorMsg));
TracingHelper.logError(processCommandSpan, errorMsg);
processCommandSpan.finish();
}
} else {
getAdapter().getMetrics().reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenantObject.getTenantId(), tenantObject, ProcessingOutcome.UNPROCESSABLE, command.getPayloadSize(), commandSample);
LOG.debug("command message is invalid: {}", command);
commandContext.reject("malformed command message");
TracingHelper.logError(processCommandSpan, "malformed command message");
processCommandSpan.finish();
}
};
final Future<CommandConsumer> commandConsumerFuture;
if (gatewayId != null) {
// gateway scenario
commandConsumerFuture = getAdapter().getCommandConsumerFactory().createCommandConsumer(tenantObject.getTenantId(), deviceId, gatewayId, commandHandler, Duration.ofSeconds(ttdSecs), waitForCommandSpan.context());
} else {
commandConsumerFuture = getAdapter().getCommandConsumerFactory().createCommandConsumer(tenantObject.getTenantId(), deviceId, commandHandler, Duration.ofSeconds(ttdSecs), waitForCommandSpan.context());
}
return commandConsumerFuture.onFailure(thr -> {
TracingHelper.logError(waitForCommandSpan, thr);
waitForCommandSpan.finish();
}).map(consumer -> {
if (!requestProcessed.get()) {
// if the request was not responded already, add a timer for triggering an empty response
addCommandReceptionTimer(context, requestProcessed, responseReady, ttdSecs, waitForCommandSpan);
context.startAcceptTimer(vertx, tenantObject, getAdapter().getConfig().getTimeoutToAck());
}
// for unregistering the command consumer (which is something the parent request span doesn't wait for)
return new CommandConsumer() {
@Override
public Future<Void> close(final SpanContext ignored) {
final Span closeConsumerSpan = TracingHelper.buildFollowsFromSpan(getTracer(), waitForCommandSpan.context(), "close consumer").withTag(Tags.COMPONENT.getKey(), getAdapter().getTypeName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, tenantObject.getTenantId()).withTag(TracingHelper.TAG_DEVICE_ID, deviceId).start();
return consumer.close(closeConsumerSpan.context()).onFailure(thr -> TracingHelper.logError(closeConsumerSpan, thr)).onComplete(ar -> closeConsumerSpan.finish());
}
};
});
}
use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapterTest method testOneWayCommandOutcome.
private void testOneWayCommandOutcome(final ProtonDelivery deviceDisposition, final Consumer<CommandContext> outcomeAssertion, final ProcessingOutcome expectedProcessingOutcome) {
// GIVEN an AMQP adapter
givenAnAdapter(properties);
// to which a device is connected
final ProtonSender deviceLink = mock(ProtonSender.class);
when(deviceLink.getQoS()).thenReturn(ProtonQoS.AT_LEAST_ONCE);
// that has subscribed to commands
final TenantObject tenantObject = givenAConfiguredTenant(TEST_TENANT_ID, true);
// WHEN an application sends a one-way command to the device
final Buffer payload = Buffer.buffer("payload");
final CommandContext context = givenAOneWayCommandContext(TEST_TENANT_ID, TEST_DEVICE, "commandToExecute", "text/plain", payload);
adapter.onCommandReceived(tenantObject, deviceLink, context);
// and the device settles it
final ArgumentCaptor<Handler<ProtonDelivery>> deliveryUpdateHandler = VertxMockSupport.argumentCaptorHandler();
verify(deviceLink).send(any(Message.class), deliveryUpdateHandler.capture());
deliveryUpdateHandler.getValue().handle(deviceDisposition);
// THEN the command is handled according to the device's disposition update
outcomeAssertion.accept(context);
// and the command has been reported according to the outcome
verify(metrics).reportCommand(eq(Direction.ONE_WAY), eq(TEST_TENANT_ID), eq(tenantObject), eq(expectedProcessingOutcome), eq(payload.length()), any());
}
use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapterTest method testLinkForSendingCommandsCloseAfterTimeout.
/**
* Verifies that the adapter closes the link for sending commands to a device when no
* delivery update is received after a certain amount of time.
*/
@Test
public void testLinkForSendingCommandsCloseAfterTimeout() {
// GIVEN an AMQP adapter
givenAnAdapter(properties);
// to which a device is connected
final ProtonSender deviceLink = mock(ProtonSender.class);
when(deviceLink.getQoS()).thenReturn(ProtonQoS.AT_LEAST_ONCE);
// that has subscribed to commands
final TenantObject tenantObject = givenAConfiguredTenant(TEST_TENANT_ID, true);
// WHEN an application sends a one-way command to the device
final Buffer payload = Buffer.buffer("payload");
final CommandContext context = givenAOneWayCommandContext(TEST_TENANT_ID, TEST_DEVICE, "commandToExecute", "text/plain", payload);
// AND no delivery update is received from the device after sometime
doAnswer(invocation -> {
final Handler<Long> task = invocation.getArgument(1);
task.handle(1L);
return 1L;
}).when(vertx).setTimer(anyLong(), VertxMockSupport.anyHandler());
adapter.onCommandReceived(tenantObject, deviceLink, context);
// THEN the adapter releases the command
verify(context).release(any(Throwable.class));
}
use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class AbstractVertxBasedHttpProtocolAdapter method createCommandConsumer.
/**
* Creates a consumer for command messages to be sent to a device.
*
* @param ttdSecs The number of seconds the device waits for a command.
* @param tenantObject The tenant configuration object.
* @param deviceId The identifier of the device.
* @param gatewayId The identifier of the gateway that is acting on behalf of the device
* or {@code null} otherwise.
* @param ctx The device's currently executing HTTP request.
* @param responseReady A future to complete once one of the following conditions are met:
* <ul>
* <li>the request did not include a <em>hono-ttd</em> parameter or</li>
* <li>a command has been received and the response ready future has not yet been
* completed or</li>
* <li>the ttd has expired</li>
* </ul>
* @param uploadMessageSpan The OpenTracing Span used for tracking the processing
* of the request.
* @return A future indicating the outcome of the operation.
* <p>
* The future will be completed with the created message consumer or {@code null}, if
* the response can be sent back to the device without waiting for a command.
* <p>
* The future will be failed with a {@code ServiceInvocationException} if the
* message consumer could not be created.
* @throws NullPointerException if any of the parameters other than TTD or gatewayId is {@code null}.
*/
protected final Future<CommandConsumer> createCommandConsumer(final Integer ttdSecs, final TenantObject tenantObject, final String deviceId, final String gatewayId, final RoutingContext ctx, final Handler<AsyncResult<Void>> responseReady, final Span uploadMessageSpan) {
Objects.requireNonNull(tenantObject);
Objects.requireNonNull(deviceId);
Objects.requireNonNull(ctx);
Objects.requireNonNull(responseReady);
Objects.requireNonNull(uploadMessageSpan);
if (ttdSecs == null || ttdSecs <= 0) {
// no need to wait for a command
responseReady.handle(Future.succeededFuture());
return Future.succeededFuture();
}
final AtomicBoolean requestProcessed = new AtomicBoolean(false);
uploadMessageSpan.setTag(MessageHelper.APP_PROPERTY_DEVICE_TTD, ttdSecs);
final Span waitForCommandSpan = TracingHelper.buildChildSpan(tracer, uploadMessageSpan.context(), "create consumer and wait for command", getTypeName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, tenantObject.getTenantId()).withTag(TracingHelper.TAG_DEVICE_ID, deviceId).start();
final Handler<CommandContext> commandHandler = commandContext -> {
final Span processCommandSpan = TracingHelper.buildFollowsFromSpan(tracer, waitForCommandSpan.context(), "process received command").withTag(Tags.COMPONENT.getKey(), getTypeName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, tenantObject.getTenantId()).withTag(TracingHelper.TAG_DEVICE_ID, deviceId).addReference(References.FOLLOWS_FROM, commandContext.getTracingContext()).start();
Tags.COMPONENT.set(commandContext.getTracingSpan(), getTypeName());
commandContext.logCommandToSpan(processCommandSpan);
final Command command = commandContext.getCommand();
final Sample commandSample = getMetrics().startTimer();
if (isCommandValid(command, processCommandSpan)) {
if (requestProcessed.compareAndSet(false, true)) {
waitForCommandSpan.finish();
checkMessageLimit(tenantObject, command.getPayloadSize(), processCommandSpan.context()).onComplete(result -> {
if (result.succeeded()) {
addMicrometerSample(commandContext, commandSample);
// put command context to routing context and notify
ctx.put(CommandContext.KEY_COMMAND_CONTEXT, commandContext);
} else {
commandContext.reject(result.cause());
TracingHelper.logError(processCommandSpan, "rejected command for device", result.cause());
metrics.reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenantObject.getTenantId(), tenantObject, ProcessingOutcome.from(result.cause()), command.getPayloadSize(), commandSample);
}
cancelCommandReceptionTimer(ctx);
setTtdStatus(ctx, TtdStatus.COMMAND);
responseReady.handle(Future.succeededFuture());
processCommandSpan.finish();
});
} else {
final String errorMsg = "waiting time for command has elapsed or another command has already been processed";
log.debug("{} [tenantId: {}, deviceId: {}]", errorMsg, tenantObject.getTenantId(), deviceId);
getMetrics().reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenantObject.getTenantId(), tenantObject, ProcessingOutcome.UNDELIVERABLE, command.getPayloadSize(), commandSample);
commandContext.release(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, errorMsg));
TracingHelper.logError(processCommandSpan, errorMsg);
processCommandSpan.finish();
}
} else {
getMetrics().reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenantObject.getTenantId(), tenantObject, ProcessingOutcome.UNPROCESSABLE, command.getPayloadSize(), commandSample);
log.debug("command message is invalid: {}", command);
commandContext.reject("malformed command message");
TracingHelper.logError(processCommandSpan, "malformed command message");
processCommandSpan.finish();
}
};
final Future<CommandConsumer> commandConsumerFuture;
if (gatewayId != null) {
// gateway scenario
commandConsumerFuture = getCommandConsumerFactory().createCommandConsumer(tenantObject.getTenantId(), deviceId, gatewayId, commandHandler, Duration.ofSeconds(ttdSecs), waitForCommandSpan.context());
} else {
commandConsumerFuture = getCommandConsumerFactory().createCommandConsumer(tenantObject.getTenantId(), deviceId, commandHandler, Duration.ofSeconds(ttdSecs), waitForCommandSpan.context());
}
return commandConsumerFuture.onFailure(thr -> {
TracingHelper.logError(waitForCommandSpan, thr);
waitForCommandSpan.finish();
}).map(consumer -> {
if (!requestProcessed.get()) {
// if the request was not responded already, add a timer for triggering an empty response
addCommandReceptionTimer(ctx, requestProcessed, responseReady, ttdSecs, waitForCommandSpan);
}
// for unregistering the command consumer (which is something the parent request span doesn't wait for)
return new CommandConsumer() {
@Override
public Future<Void> close(final SpanContext ignored) {
final Span closeConsumerSpan = TracingHelper.buildFollowsFromSpan(tracer, waitForCommandSpan.context(), "close consumer").withTag(Tags.COMPONENT.getKey(), getTypeName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, tenantObject.getTenantId()).withTag(TracingHelper.TAG_DEVICE_ID, deviceId).start();
return consumer.close(closeConsumerSpan.context()).onFailure(thr -> TracingHelper.logError(closeConsumerSpan, thr)).onComplete(ar -> closeConsumerSpan.finish());
}
};
});
}
use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class VertxBasedHttpProtocolAdapterTest method testPostTelemetryWithTtdSucceedsWithCommandInResponse.
/**
* Verifies that the adapter includes a command for the device in the response to
* a POST request which contains a time-til-disconnect.
*
* @param ctx The vert.x test context.
*/
@Test
public void testPostTelemetryWithTtdSucceedsWithCommandInResponse(final VertxTestContext ctx) {
// GIVEN an device for which a command is pending
givenATelemetrySenderForAnyTenant();
mockSuccessfulAuthentication("DEFAULT_TENANT", "device_1");
final CommandContext commandContext = givenARequestResponseCommandContext("DEFAULT_TENANT", "device_1", "doThis", "reply-to-id", null, null, MessagingType.amqp);
final CommandConsumer commandConsumer = mock(CommandConsumer.class);
when(commandConsumer.close(any())).thenReturn(Future.succeededFuture());
when(commandConsumerFactory.createCommandConsumer(eq("DEFAULT_TENANT"), eq("device_1"), VertxMockSupport.anyHandler(), any(), any())).thenAnswer(invocation -> {
final Handler<CommandContext> consumer = invocation.getArgument(2);
consumer.handle(commandContext);
return Future.succeededFuture(commandConsumer);
});
// WHEN the device posts a telemetry message including a TTD
httpClient.post("/telemetry").addQueryParam("hono-ttd", "3").putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpUtils.CONTENT_TYPE_JSON).basicAuthentication("testuser@DEFAULT_TENANT", "password123").putHeader(HttpHeaders.ORIGIN.toString(), ORIGIN_HEADER_VALUE).expect(ResponsePredicate.SC_OK).expect(this::assertCorsHeaders).expect(response -> {
if (!"doThis".equals(response.getHeader(Constants.HEADER_COMMAND))) {
return ResponsePredicateResult.failure("response does not contain expected hono-command header");
}
if (response.getHeader(Constants.HEADER_COMMAND_REQUEST_ID) == null) {
return ResponsePredicateResult.failure("response does not contain hono-cmd-req-id header");
}
return ResponsePredicateResult.success();
}).sendJsonObject(new JsonObject(), ctx.succeeding(r -> {
ctx.verify(() -> {
verify(commandConsumerFactory).createCommandConsumer(eq("DEFAULT_TENANT"), eq("device_1"), VertxMockSupport.anyHandler(), any(), any());
// and the command consumer has been closed again
verify(commandConsumer).close(any());
verify(commandContext).accept();
});
ctx.completeNow();
}));
}
Aggregations