use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class LoraProtocolAdapter method handleTenantTimeout.
private void handleTenantTimeout(final Message<String> msg) {
final String tenantId = msg.body();
log.debug("check command subscriptions on timeout of tenant [{}]", tenantId);
final Span span = TracingHelper.buildSpan(tracer, null, "check command subscriptions on tenant timeout", getClass().getSimpleName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).start();
TracingHelper.setDeviceTags(span, tenantId, null);
// check if tenant still exists
getTenantConfiguration(tenantId, span.context()).recover(thr -> {
if (thr instanceof TenantDisabledOrNotRegisteredException) {
log.debug("tenant [{}] disabled or removed, removing corresponding command consumers", tenantId);
span.log("tenant disabled or removed, corresponding command consumers will be closed");
@SuppressWarnings("rawtypes") final List<Future> consumerCloseFutures = new LinkedList<>();
for (final var iter = commandSubscriptions.entrySet().iterator(); iter.hasNext(); ) {
final var entry = iter.next();
if (entry.getKey().getTenant().equals(tenantId)) {
final CommandConsumer commandConsumer = entry.getValue().one();
consumerCloseFutures.add(commandConsumer.close(span.context()));
iter.remove();
}
}
return CompositeFuture.join(consumerCloseFutures).mapEmpty();
} else {
return Future.failedFuture(thr);
}
}).onFailure(thr -> TracingHelper.logError(span, thr)).onComplete(ar -> span.finish());
}
use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class LoraProtocolAdapterTest method handleCommandForLNS.
/**
* Verifies that an uplink message triggers a command subscription.
*/
@SuppressWarnings("unchecked")
@Test
public void handleCommandForLNS() {
givenATelemetrySenderForAnyTenant();
final LoraProvider providerMock = getLoraProviderMock();
final HttpServerRequest request = mock(HttpServerRequest.class);
final HttpContext httpContext = newHttpContext();
when(httpContext.request()).thenReturn(request);
final CommandEndpoint commandEndpoint = new CommandEndpoint();
commandEndpoint.setHeaders(Map.of("my-header", "my-header-value"));
commandEndpoint.setUri("https://my-server.com/commands/{{deviceId}}/send");
setGatewayDeviceCommandEndpoint(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);
final ArgumentCaptor<Handler<CommandContext>> handlerArgumentCaptor = VertxMockSupport.argumentCaptorHandler();
verify(commandConsumerFactory).createCommandConsumer(eq(TEST_TENANT_ID), eq(TEST_GATEWAY_ID), handlerArgumentCaptor.capture(), isNull(), any());
final Handler<CommandContext> commandHandler = handlerArgumentCaptor.getValue();
final Command command = mock(Command.class);
when(command.getTenant()).thenReturn(TEST_TENANT_ID);
when(command.getDeviceId()).thenReturn(TEST_DEVICE_ID);
when(command.getGatewayId()).thenReturn(TEST_GATEWAY_ID);
when(command.getPayload()).thenReturn(Buffer.buffer("bumlux"));
when(command.isValid()).thenReturn(true);
final CommandContext commandContext = mock(CommandContext.class);
when(commandContext.getCommand()).thenReturn(command);
when(commandContext.getTracingSpan()).thenReturn(processMessageSpan);
final JsonObject json = new JsonObject().put("my-payload", "bumlux");
final LoraCommand loraCommand = new LoraCommand(json, "https://my-server.com/commands/deviceId/send");
when(providerMock.getCommand(any(), any(), any(), any())).thenReturn(loraCommand);
when(providerMock.getDefaultHeaders()).thenReturn(Map.of("my-provider-header", "my-provider-header-value"));
final HttpRequest<Buffer> httpClientRequest = mock(HttpRequest.class, withSettings().defaultAnswer(RETURNS_SELF));
final HttpResponse<Buffer> httpResponse = mock(HttpResponse.class);
when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_NO_CONTENT);
when(httpClientRequest.sendJson(any(JsonObject.class))).thenReturn(Future.succeededFuture(httpResponse));
when(webClient.postAbs(anyString())).thenReturn(httpClientRequest);
commandHandler.handle(commandContext);
verify(webClient, times(1)).postAbs("https://my-server.com/commands/deviceId/send");
verify(httpClientRequest, times(1)).putHeader("my-header", "my-header-value");
verify(httpClientRequest, times(1)).putHeader("my-provider-header", "my-provider-header-value");
verify(httpClientRequest, times(1)).sendJson(json);
}
use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class AmqpAdapterClientCommandConsumerTest method testReceiverIsRecreatedOnConnectionFailure.
/**
* Verifies that the proton receiver is recreated after a reconnect.
*/
@Test
public void testReceiverIsRecreatedOnConnectionFailure() {
final AtomicReference<ReconnectListener<HonoConnection>> reconnectListener = new AtomicReference<>();
doAnswer(invocation -> {
reconnectListener.set(invocation.getArgument(0));
return null;
}).when(connection).addReconnectListener(any());
// GIVEN a connected command consumer
@SuppressWarnings("unchecked") final Future<CommandConsumer> consumerFuture = AmqpAdapterClientCommandConsumer.create(connection, mock(BiConsumer.class));
final AmqpAdapterClientCommandConsumer commandConsumer = (AmqpAdapterClientCommandConsumer) consumerFuture.result();
// WHEN the connection is re-established
final ProtonReceiver newReceiver = createNewProtonReceiver(connection);
reconnectListener.get().onReconnect(null);
// THEN the receiver is recreated
verify(connection, times(2)).createReceiver(eq("command"), eq(ProtonQoS.AT_LEAST_ONCE), any(ProtonMessageHandler.class), VertxMockSupport.anyHandler());
final ProtonReceiver actual = commandConsumer.getReceiver();
assertThat(actual).isNotEqualTo(originalReceiver);
assertThat(actual).isEqualTo(newReceiver);
}
use of org.eclipse.hono.client.command.CommandConsumer 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.CommandConsumer in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapterTest method testAdapterSkipsTtdEventOnCmdConnectionCloseIfRemoveConsumerFails.
/**
* Verifies that the adapter doesn't send a 'disconnectedTtdEvent' on connection loss
* when removal of the command consumer mapping entry fails (which would be the case
* when another command consumer mapping had been registered in the mean time, meaning
* the device has already reconnected).
*
* @param ctx The vert.x test context.
* @throws InterruptedException if the test execution gets interrupted.
*/
@Test
public void testAdapterSkipsTtdEventOnCmdConnectionCloseIfRemoveConsumerFails(final VertxTestContext ctx) 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.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_PRECON_FAILED)));
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
final ArgumentCaptor<Handler<AsyncResult<ProtonConnection>>> closeHandler = VertxMockSupport.argumentCaptorHandler();
verify(deviceConnection).closeHandler(closeHandler.capture());
closeHandler.getValue().handle(Future.succeededFuture(deviceConnection));
// THEN the adapter closes the command consumer
verify(commandConsumer).close(any());
// and since closing the command consumer fails with a precon-failed exception
// there is only one notification sent during consumer creation,
assertEmptyNotificationHasBeenSentDownstream(TEST_TENANT_ID, TEST_DEVICE, -1);
// no 'disconnectedTtdEvent' event with TTD = 0
assertEmptyNotificationHasNotBeenSentDownstream(TEST_TENANT_ID, TEST_DEVICE, 0);
}
Aggregations