use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapterTest method testAdapterClosesCommandConsumer.
/**
* Verifies that the adapter closes a corresponding command consumer if
* the connection to a device fails unexpectedly.
*
* @param ctx The vert.x test context.
* @throws InterruptedException if the test execution gets interrupted.
*/
private void testAdapterClosesCommandConsumer(final VertxTestContext ctx, final Handler<ProtonConnection> connectionLossTrigger) throws InterruptedException {
// GIVEN an AMQP adapter
givenAnAdapter(properties);
givenAnEventSenderForAnyTenant();
final Promise<Void> startupTracker = Promise.promise();
startupTracker.future().onComplete(ctx.succeedingThenComplete());
adapter.start(startupTracker);
assertThat(ctx.awaitCompletion(2, TimeUnit.SECONDS)).isTrue();
// to which a device is connected
final Device authenticatedDevice = new Device(TEST_TENANT_ID, TEST_DEVICE);
final Record record = new RecordImpl();
record.set(AmqpAdapterConstants.KEY_CLIENT_DEVICE, Device.class, authenticatedDevice);
final ProtonConnection deviceConnection = mock(ProtonConnection.class);
when(deviceConnection.attachments()).thenReturn(record);
final ArgumentCaptor<Handler<ProtonConnection>> connectHandler = VertxMockSupport.argumentCaptorHandler();
verify(server).connectHandler(connectHandler.capture());
connectHandler.getValue().handle(deviceConnection);
// that wants to receive commands
final CommandConsumer commandConsumer = mock(CommandConsumer.class);
when(commandConsumer.close(any())).thenReturn(Future.succeededFuture());
when(commandConsumerFactory.createCommandConsumer(eq(TEST_TENANT_ID), eq(TEST_DEVICE), VertxMockSupport.anyHandler(), any(), any())).thenReturn(Future.succeededFuture(commandConsumer));
final String sourceAddress = getCommandEndpoint();
final ProtonSender sender = getSender(sourceAddress);
adapter.handleRemoteSenderOpenForCommands(deviceConnection, sender);
// WHEN the connection to the device is lost
connectionLossTrigger.handle(deviceConnection);
// THEN the adapter closes the command consumer
verify(commandConsumer).close(any());
// and sends an empty event with TTD = 0 downstream
assertEmptyNotificationHasBeenSentDownstream(TEST_TENANT_ID, TEST_DEVICE, 0);
}
use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapterTest method testAdapterClosesCommandConsumerWhenDeviceClosesReceiverLink.
/**
* Verify that if a client device closes the link for receiving commands, then the AMQP
* adapter sends an empty notification downstream with TTD 0 and closes the command
* consumer.
*/
@Test
public void testAdapterClosesCommandConsumerWhenDeviceClosesReceiverLink() {
// GIVEN an AMQP adapter
givenAnAdapter(properties);
givenAnEventSenderForAnyTenant();
// and a device that wants to receive commands
final CommandConsumer commandConsumer = mock(CommandConsumer.class);
when(commandConsumer.close(any())).thenReturn(Future.succeededFuture());
when(commandConsumerFactory.createCommandConsumer(eq(TEST_TENANT_ID), eq(TEST_DEVICE), VertxMockSupport.anyHandler(), any(), any())).thenReturn(Future.succeededFuture(commandConsumer));
final String sourceAddress = String.format("%s", getCommandEndpoint());
final ProtonSender sender = getSender(sourceAddress);
final Device authenticatedDevice = new Device(TEST_TENANT_ID, TEST_DEVICE);
final ProtonConnection deviceConnection = mock(ProtonConnection.class);
final Record attachments = mock(Record.class);
when(attachments.get(AmqpAdapterConstants.KEY_CLIENT_DEVICE, Device.class)).thenReturn(authenticatedDevice);
when(deviceConnection.attachments()).thenReturn(attachments);
adapter.handleRemoteSenderOpenForCommands(deviceConnection, sender);
// WHEN the client device closes its receiver link (unsubscribe)
final ArgumentCaptor<Handler<AsyncResult<ProtonSender>>> closeHookCaptor = VertxMockSupport.argumentCaptorHandler();
verify(sender).closeHandler(closeHookCaptor.capture());
closeHookCaptor.getValue().handle(null);
// THEN the adapter closes the command consumer
verify(commandConsumer).close(any());
// AND sends an empty notification downstream
assertEmptyNotificationHasBeenSentDownstream(TEST_TENANT_ID, TEST_DEVICE, 0);
}
use of org.eclipse.hono.client.command.CommandConsumer 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.CommandConsumer in project hono by eclipse.
the class LoraProtocolAdapterTest method handleProviderRouteSuccessfullyForUplinkMessage.
/**
* Verifies that an uplink message is routed to a provider correctly.
*/
@Test
public void handleProviderRouteSuccessfullyForUplinkMessage() {
givenATelemetrySenderForAnyTenant();
final LoraProvider providerMock = getLoraProviderMock();
final HttpServerRequest request = mock(HttpServerRequest.class);
final HttpContext httpContext = newHttpContext();
when(httpContext.request()).thenReturn(request);
setGatewayDeviceCommandEndpoint(new CommandEndpoint());
final CommandConsumer commandConsumer = mock(CommandConsumer.class);
when(commandConsumer.close(any())).thenReturn(Future.succeededFuture());
when(commandConsumerFactory.createCommandConsumer(any(), any(), any(), any(), any())).thenReturn(Future.succeededFuture(commandConsumer));
adapter.handleProviderRoute(httpContext, providerMock);
verify(httpContext.getRoutingContext()).put(LoraConstants.APP_PROPERTY_ORIG_LORA_PROVIDER, TEST_PROVIDER);
@SuppressWarnings("unchecked") final ArgumentCaptor<Map<String, Object>> props = ArgumentCaptor.forClass(Map.class);
verify(telemetrySender).sendTelemetry(argThat(tenant -> TEST_TENANT_ID.equals(tenant.getTenantId())), argThat(assertion -> TEST_DEVICE_ID.equals(assertion.getDeviceId())), eq(QoS.AT_MOST_ONCE), eq(LoraConstants.CONTENT_TYPE_LORA_BASE + TEST_PROVIDER), eq(Buffer.buffer(TEST_PAYLOAD)), props.capture(), any());
assertThat(props.getValue()).containsEntry(LoraConstants.APP_PROPERTY_FUNCTION_PORT, TEST_FUNCTION_PORT);
final String metaData = (String) props.getValue().get(LoraConstants.APP_PROPERTY_META_DATA);
assertThat(metaData).isNotNull();
final JsonObject metaDataJson = new JsonObject(metaData);
assertThat(metaDataJson.getInteger(LoraConstants.APP_PROPERTY_FUNCTION_PORT)).isEqualTo(TEST_FUNCTION_PORT);
verify(httpContext.getRoutingContext().response()).setStatusCode(HttpResponseStatus.ACCEPTED.code());
verify(processMessageSpan).finish();
}
use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class LoraProtocolAdapterTest method handleCommandSubscriptionSuccessfullyForUplinkMessage.
/**
* Verifies that an uplink message triggers a command subscription.
*/
@Test
public void handleCommandSubscriptionSuccessfullyForUplinkMessage() {
givenATelemetrySenderForAnyTenant();
final LoraProvider providerMock = getLoraProviderMock();
final HttpServerRequest request = mock(HttpServerRequest.class);
final HttpContext httpContext = newHttpContext();
when(httpContext.request()).thenReturn(request);
setGatewayDeviceCommandEndpoint(new CommandEndpoint());
final CommandConsumer commandConsumer = mock(CommandConsumer.class);
when(commandConsumer.close(any())).thenReturn(Future.succeededFuture());
when(commandConsumerFactory.createCommandConsumer(any(), any(), any(), any(), any())).thenReturn(Future.succeededFuture(commandConsumer));
adapter.handleProviderRoute(httpContext, providerMock);
verify(commandConsumerFactory).createCommandConsumer(eq("myTenant"), eq("myLoraGateway"), VertxMockSupport.anyHandler(), isNull(), any());
}
Aggregations