use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class AbstractHonoResource method doUploadMessage.
/**
* Forwards a message to the south bound Telemetry or Event API of the messaging infrastructure configured
* for the tenant that the origin device belongs to.
* <p>
* Depending on the outcome of the attempt to upload the message, the CoAP response code is set as
* described by the <a href="https://www.eclipse.org/hono/docs/user-guide/coap-adapter/">CoAP adapter user guide</a>
*
* @param context The request that contains the uploaded message.
* @param endpoint The type of API endpoint to forward the message to.
* @return A future indicating the outcome of the operation.
* The future will be succeeded if the message has been forwarded successfully.
* In this case one of the context's <em>respond</em> methods will have been invoked to send a CoAP response
* back to the device.
* Otherwise the future will be failed with a {@link org.eclipse.hono.client.ServiceInvocationException}.
* @throws NullPointerException if any of the parameters are {@code null}.
*/
protected final Future<Void> doUploadMessage(final CoapContext context, final MetricsTags.EndpointType endpoint) {
Objects.requireNonNull(context);
Objects.requireNonNull(endpoint);
final String contentType = context.getContentType();
final Buffer payload = context.getPayload();
if (contentType == null) {
return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "request message must contain content-format option"));
} else if (payload.length() == 0 && !context.isEmptyNotification()) {
return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "request contains no body but is not marked as empty notification"));
} else {
final String gatewayId = context.getGatewayId();
final String tenantId = context.getOriginDevice().getTenantId();
final String deviceId = context.getOriginDevice().getDeviceId();
final MetricsTags.QoS qos = context.isConfirmable() ? MetricsTags.QoS.AT_LEAST_ONCE : MetricsTags.QoS.AT_MOST_ONCE;
final Span currentSpan = TracingHelper.buildChildSpan(getTracer(), context.getTracingContext(), "upload " + endpoint.getCanonicalName(), getAdapter().getTypeName()).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).withTag(TracingHelper.TAG_TENANT_ID, tenantId).withTag(TracingHelper.TAG_DEVICE_ID, deviceId).withTag(TracingHelper.TAG_AUTHENTICATED.getKey(), context.isDeviceAuthenticated()).withTag(Constants.HEADER_QOS_LEVEL, qos.asTag().getValue()).start();
final Promise<Void> responseReady = Promise.promise();
final Future<RegistrationAssertion> tokenTracker = getAdapter().getRegistrationAssertion(tenantId, deviceId, context.getAuthenticatedDevice(), currentSpan.context());
final Future<TenantObject> tenantTracker = getAdapter().getTenantClient().get(tenantId, currentSpan.context());
final Future<TenantObject> tenantValidationTracker = tenantTracker.compose(tenantObject -> CompositeFuture.all(getAdapter().isAdapterEnabled(tenantObject), getAdapter().checkMessageLimit(tenantObject, payload.length(), currentSpan.context())).map(tenantObject));
// we only need to consider TTD if the device and tenant are enabled and the adapter
// is enabled for the tenant
final Future<Integer> ttdTracker = CompositeFuture.all(tenantValidationTracker, tokenTracker).compose(ok -> {
final Integer ttdParam = context.getTimeUntilDisconnect();
return getAdapter().getTimeUntilDisconnect(tenantTracker.result(), ttdParam).map(effectiveTtd -> {
if (effectiveTtd != null) {
currentSpan.setTag(MessageHelper.APP_PROPERTY_DEVICE_TTD, effectiveTtd);
}
return effectiveTtd;
});
});
final Future<CommandConsumer> commandConsumerTracker = ttdTracker.compose(ttd -> createCommandConsumer(ttd, tenantTracker.result(), deviceId, gatewayId, context, responseReady, currentSpan));
return commandConsumerTracker.compose(commandConsumer -> {
final Map<String, Object> props = getAdapter().getDownstreamMessageProperties(context);
Optional.ofNullable(commandConsumer).map(c -> ttdTracker.result()).ifPresent(ttd -> props.put(MessageHelper.APP_PROPERTY_DEVICE_TTD, ttd));
customizeDownstreamMessageProperties(props, context);
if (context.isConfirmable()) {
context.startAcceptTimer(vertx, tenantTracker.result(), getAdapter().getConfig().getTimeoutToAck());
}
final Future<Void> sendResult;
if (endpoint == EndpointType.EVENT) {
sendResult = getAdapter().getEventSender(tenantValidationTracker.result()).sendEvent(tenantTracker.result(), tokenTracker.result(), contentType, payload, props, currentSpan.context());
} else {
sendResult = getAdapter().getTelemetrySender(tenantValidationTracker.result()).sendTelemetry(tenantTracker.result(), tokenTracker.result(), context.getRequestedQos(), contentType, payload, props, currentSpan.context());
}
return CompositeFuture.all(sendResult, responseReady.future()).mapEmpty();
}).compose(proceed -> {
// request and the CommandConsumer from the current request has not been closed yet
return Optional.ofNullable(commandConsumerTracker.result()).map(consumer -> consumer.close(currentSpan.context()).otherwise(thr -> {
TracingHelper.logError(currentSpan, thr);
return (Void) null;
})).orElseGet(Future::succeededFuture);
}).map(proceed -> {
final CommandContext commandContext = context.get(CommandContext.KEY_COMMAND_CONTEXT);
final Response response = new Response(ResponseCode.CHANGED);
if (commandContext != null) {
addCommandToResponse(response, commandContext, currentSpan);
commandContext.accept();
getAdapter().getMetrics().reportCommand(commandContext.getCommand().isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenantId, tenantTracker.result(), ProcessingOutcome.FORWARDED, commandContext.getCommand().getPayloadSize(), getMicrometerSample(commandContext));
}
LOG.trace("successfully processed message for device [tenantId: {}, deviceId: {}, endpoint: {}]", tenantId, deviceId, endpoint.getCanonicalName());
getAdapter().getMetrics().reportTelemetry(endpoint, tenantId, tenantTracker.result(), MetricsTags.ProcessingOutcome.FORWARDED, qos, payload.length(), getTtdStatus(context), context.getTimer());
context.respond(response);
currentSpan.finish();
return (Void) null;
}).recover(t -> {
LOG.debug("cannot process message from device [tenantId: {}, deviceId: {}, endpoint: {}]", tenantId, deviceId, endpoint.getCanonicalName(), t);
final Future<Void> commandConsumerClosedTracker = Optional.ofNullable(commandConsumerTracker.result()).map(consumer -> consumer.close(currentSpan.context()).onFailure(thr -> TracingHelper.logError(currentSpan, thr))).orElseGet(Future::succeededFuture);
final CommandContext commandContext = context.get(CommandContext.KEY_COMMAND_CONTEXT);
if (commandContext != null) {
TracingHelper.logError(commandContext.getTracingSpan(), "command won't be forwarded to device in CoAP response, CoAP request handling failed", t);
commandContext.release(t);
currentSpan.log("released command for device");
}
getAdapter().getMetrics().reportTelemetry(endpoint, tenantId, tenantTracker.result(), ClientErrorException.class.isInstance(t) ? MetricsTags.ProcessingOutcome.UNPROCESSABLE : MetricsTags.ProcessingOutcome.UNDELIVERABLE, qos, payload.length(), getTtdStatus(context), context.getTimer());
TracingHelper.logError(currentSpan, t);
commandConsumerClosedTracker.onComplete(res -> currentSpan.finish());
return Future.failedFuture(t);
});
}
}
use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapterTest method testDeviceConnectionIsClosedOnDeviceOrTenantChangeNotification.
private <T extends AbstractNotification> void testDeviceConnectionIsClosedOnDeviceOrTenantChangeNotification(final T notification) {
// GIVEN an AMQP adapter
givenAnAdapter(properties);
final Promise<Void> startPromise = Promise.promise();
adapter.doStart(startPromise);
assertThat(startPromise.future().succeeded()).isTrue();
@SuppressWarnings("unchecked") final ArgumentCaptor<Handler<io.vertx.core.eventbus.Message<T>>> notificationHandlerCaptor = getEventBusConsumerHandlerArgumentCaptor((NotificationType<T>) notification.getType());
// with an enabled tenant
givenAConfiguredTenant(TEST_TENANT_ID, true);
// WHEN a device connects
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);
when(deviceConnection.getRemoteContainer()).thenReturn("deviceContainer");
adapter.onConnectRequest(deviceConnection);
final ArgumentCaptor<Handler<AsyncResult<ProtonConnection>>> openHandler = VertxMockSupport.argumentCaptorHandler();
verify(deviceConnection).openHandler(openHandler.capture());
openHandler.getValue().handle(Future.succeededFuture(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);
// THEN the connection count is incremented
verify(metrics).incrementConnections(TEST_TENANT_ID);
// AND WHEN a notification is sent about the tenant/device having been deleted or disabled
sendViaEventBusMock(notification, notificationHandlerCaptor.getValue());
// THEN the device connection is closed
verify(deviceConnection).close();
// and the connection count is decremented
verify(metrics).decrementConnections(TEST_TENANT_ID);
// and the adapter has closed the command consumer
verify(commandConsumer).close(any());
}
use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapter method openCommandSenderLink.
private Future<CommandConsumer> openCommandSenderLink(final ProtonConnection connection, final ProtonSender sender, final ResourceIdentifier address, final Device authenticatedDevice, final Span span, final OptionalInt traceSamplingPriority) {
return createCommandConsumer(sender, address, authenticatedDevice, span).map(consumer -> {
final String tenantId = address.getTenantId();
final String deviceId = address.getResourceId();
sender.setSource(sender.getRemoteSource());
sender.setTarget(sender.getRemoteTarget());
sender.setQoS(ProtonQoS.AT_LEAST_ONCE);
final Handler<AsyncResult<ProtonSender>> detachHandler = link -> {
final Span detachHandlerSpan = newSpan("detach device command receiver link", authenticatedDevice, traceSamplingPriority);
removeCommandSubscription(connection, address.toString());
onLinkDetach(sender);
closeCommandConsumer(consumer, address, authenticatedDevice, true, detachHandlerSpan).onComplete(v -> detachHandlerSpan.finish());
};
HonoProtonHelper.setCloseHandler(sender, detachHandler);
HonoProtonHelper.setDetachHandler(sender, detachHandler);
sender.open();
// At this point, the remote peer's receiver link is successfully opened and is ready to receive
// commands. Send "device ready for command" notification downstream.
log.debug("established link [address: {}] for sending commands to device", address);
sendConnectedTtdEvent(tenantId, deviceId, authenticatedDevice, span.context());
registerCommandSubscription(connection, new CommandSubscription(consumer, address));
return consumer;
}).recover(t -> Future.failedFuture(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "cannot create command consumer")));
}
use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class AbstractVertxBasedMqttProtocolAdapterTest 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).
*/
@Test
public void testAdapterSkipsTtdEventOnCmdConnectionCloseIfRemoveConsumerFails() {
// GIVEN a device connected to an adapter
givenAnAdapter(properties);
givenAnEventSenderForAnyTenant();
final MqttEndpoint endpoint = mockEndpoint();
when(endpoint.isConnected()).thenReturn(true);
// 10 seconds
when(endpoint.keepAliveTimeSeconds()).thenReturn(10);
// WHEN a device subscribes to commands
final CommandConsumer commandConsumer = mock(CommandConsumer.class);
when(commandConsumer.close(any())).thenReturn(Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_PRECON_FAILED)));
when(commandConsumerFactory.createCommandConsumer(eq("tenant"), eq("deviceId"), VertxMockSupport.anyHandler(), any(), any())).thenReturn(Future.succeededFuture(commandConsumer));
final List<MqttTopicSubscription> subscriptions = Collections.singletonList(newMockTopicSubscription(getCommandSubscriptionTopic("tenant", "deviceId"), MqttQoS.AT_MOST_ONCE));
final MqttSubscribeMessage msg = mock(MqttSubscribeMessage.class);
when(msg.messageId()).thenReturn(15);
when(msg.topicSubscriptions()).thenReturn(subscriptions);
final var mqttDeviceEndpoint = adapter.createMqttDeviceEndpoint(endpoint, null, OptionalInt.empty());
endpoint.closeHandler(handler -> mqttDeviceEndpoint.onClose());
mqttDeviceEndpoint.onSubscribe(msg);
// THEN the adapter creates a command consumer that is checked periodically
verify(commandConsumerFactory).createCommandConsumer(eq("tenant"), eq("deviceId"), VertxMockSupport.anyHandler(), any(), any());
// and the adapter registers a hook on the connection to the device
final ArgumentCaptor<Handler<Void>> closeHookCaptor = VertxMockSupport.argumentCaptorHandler();
verify(endpoint).closeHandler(closeHookCaptor.capture());
// which closes the command consumer when the device disconnects
closeHookCaptor.getValue().handle(null);
when(endpoint.isConnected()).thenReturn(false);
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("tenant", "deviceId", -1);
// no 'disconnectedTtdEvent' event with TTD = 0
assertEmptyNotificationHasNotBeenSentDownstream("tenant", "deviceId", 0);
}
use of org.eclipse.hono.client.command.CommandConsumer in project hono by eclipse.
the class LoraProtocolAdapter method registerCommandConsumerIfNeeded.
private void registerCommandConsumerIfNeeded(final LoraProvider provider, final Device gatewayDevice, final SpanContext context) {
final String tenantId = gatewayDevice.getTenantId();
final String gatewayId = gatewayDevice.getDeviceId();
final SubscriptionKey key = new SubscriptionKey(tenantId, gatewayId);
if (commandSubscriptions.containsKey(key)) {
return;
}
// use FOLLOWS_FROM span since this operation is decoupled from the rest of the request handling
final Span currentSpan = TracingHelper.buildFollowsFromSpan(tracer, context, "create command consumer").withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT).start();
TracingHelper.setDeviceTags(currentSpan, tenantId, gatewayId);
TAG_LORA_PROVIDER.set(currentSpan, provider.getProviderName());
getRegistrationClient().assertRegistration(tenantId, gatewayId, null, currentSpan.context()).onFailure(thr -> {
LOG.debug("error asserting gateway registration, no command consumer will be created [tenant: {}, gateway-id: {}]", tenantId, gatewayId);
TracingHelper.logError(currentSpan, "error asserting gateway registration, no command consumer will be created", thr);
}).compose(assertion -> {
if (assertion.getCommandEndpoint() == null) {
LOG.debug("gateway has no command endpoint defined, skipping command consumer creation [tenant: {}, gateway-id: {}]", tenantId, gatewayId);
currentSpan.log("gateway has no command endpoint defined, skipping command consumer creation");
return Future.succeededFuture((Void) null);
}
return getCommandConsumerFactory().createCommandConsumer(tenantId, gatewayId, this::handleCommand, null, currentSpan.context()).onFailure(thr -> TracingHelper.logError(currentSpan, thr)).map(commandConsumer -> commandSubscriptions.put(key, Pair.of(commandConsumer, provider))).mapEmpty();
}).onComplete(ar -> currentSpan.finish());
}
Aggregations