use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapter method onCommandReceived.
/**
* Invoked for every valid command that has been received from
* an application.
* <p>
* This implementation simply forwards the command to the device
* via the given link.
*
* @param tenantObject The tenant configuration object.
* @param sender The link for sending the command to the device.
* @param commandContext The context in which the adapter receives the command message.
* @throws NullPointerException if any of the parameters is {@code null}.
*/
protected void onCommandReceived(final TenantObject tenantObject, final ProtonSender sender, final CommandContext commandContext) {
Objects.requireNonNull(tenantObject);
Objects.requireNonNull(sender);
Objects.requireNonNull(commandContext);
final Command command = commandContext.getCommand();
final AtomicBoolean isCommandSettled = new AtomicBoolean(false);
if (sender.sendQueueFull()) {
log.debug("cannot send command to device: no credit available [{}]", command);
commandContext.release(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "no credit available for sending command to device"));
reportSentCommand(tenantObject, commandContext, ProcessingOutcome.UNDELIVERABLE);
} else {
final Message msg = ProtonHelper.message();
msg.setAddress(String.format("%s/%s/%s", CommandConstants.COMMAND_ENDPOINT, command.getTenant(), command.getDeviceId()));
msg.setCorrelationId(command.getCorrelationId());
msg.setSubject(command.getName());
MessageHelper.setPayload(msg, command.getContentType(), command.getPayload());
if (command.isTargetedAtGateway()) {
MessageHelper.addDeviceId(msg, command.getDeviceId());
}
if (!command.isOneWay()) {
msg.setReplyTo(String.format("%s/%s/%s", CommandConstants.COMMAND_RESPONSE_ENDPOINT, command.getTenant(), Commands.getDeviceFacingReplyToId(command.getReplyToId(), command.getDeviceId(), command.getMessagingType())));
}
final Long timerId;
if (getConfig().getSendMessageToDeviceTimeout() < 1) {
timerId = null;
} else {
timerId = vertx.setTimer(getConfig().getSendMessageToDeviceTimeout(), tid -> {
if (log.isDebugEnabled()) {
final String linkOrConnectionClosedInfo = HonoProtonHelper.isLinkOpenAndConnected(sender) ? "" : " (link or connection already closed)";
log.debug("waiting for delivery update timed out after {}ms{} [{}]", getConfig().getSendMessageToDeviceTimeout(), linkOrConnectionClosedInfo, command);
}
if (isCommandSettled.compareAndSet(false, true)) {
// timeout reached -> release command
commandContext.release(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "timeout waiting for delivery update from device"));
reportSentCommand(tenantObject, commandContext, ProcessingOutcome.UNDELIVERABLE);
} else if (log.isTraceEnabled()) {
log.trace("command is already settled and downstream application was already notified [{}]", command);
}
});
}
sender.send(msg, delivery -> {
if (timerId != null) {
// disposition received -> cancel timer
vertx.cancelTimer(timerId);
}
if (!isCommandSettled.compareAndSet(false, true)) {
log.trace("command is already settled and downstream application was already notified [{}]", command);
} else {
// release the command message when the device either
// rejects or does not settle the command request message.
final DeliveryState remoteState = delivery.getRemoteState();
ProcessingOutcome outcome = null;
if (delivery.remotelySettled()) {
if (Accepted.class.isInstance(remoteState)) {
outcome = ProcessingOutcome.FORWARDED;
commandContext.accept();
} else if (Rejected.class.isInstance(remoteState)) {
outcome = ProcessingOutcome.UNPROCESSABLE;
final String cause = Optional.ofNullable(((Rejected) remoteState).getError()).map(ErrorCondition::getDescription).orElse(null);
commandContext.reject(cause);
} else if (Released.class.isInstance(remoteState)) {
outcome = ProcessingOutcome.UNDELIVERABLE;
commandContext.release();
} else if (Modified.class.isInstance(remoteState)) {
final Modified modified = (Modified) remoteState;
outcome = modified.getUndeliverableHere() ? ProcessingOutcome.UNPROCESSABLE : ProcessingOutcome.UNDELIVERABLE;
commandContext.modify(modified.getDeliveryFailed(), modified.getUndeliverableHere());
}
} else {
log.debug("device did not settle command message [remote state: {}, {}]", remoteState, command);
final Map<String, Object> logItems = new HashMap<>(2);
logItems.put(Fields.EVENT, "device did not settle command");
logItems.put("remote state", remoteState);
commandContext.getTracingSpan().log(logItems);
commandContext.release(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "device did not settle command"));
outcome = ProcessingOutcome.UNDELIVERABLE;
}
reportSentCommand(tenantObject, commandContext, outcome);
}
});
final Map<String, Object> items = new HashMap<>(4);
items.put(Fields.EVENT, "command sent to device");
if (sender.getRemoteTarget() != null) {
items.put(Tags.MESSAGE_BUS_DESTINATION.getKey(), sender.getRemoteTarget().getAddress());
}
items.put(TracingHelper.TAG_QOS.getKey(), sender.getQoS().name());
items.put(TracingHelper.TAG_CREDIT.getKey(), sender.getCredit());
commandContext.getTracingSpan().log(items);
}
}
use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapter method createCommandConsumer.
private Future<CommandConsumer> createCommandConsumer(final ProtonSender sender, final ResourceIdentifier sourceAddress, final Device authenticatedDevice, final Span span) {
final Handler<CommandContext> commandHandler = commandContext -> {
final Sample timer = metrics.startTimer();
addMicrometerSample(commandContext, timer);
Tags.COMPONENT.set(commandContext.getTracingSpan(), getTypeName());
final Command command = commandContext.getCommand();
final Future<TenantObject> tenantTracker = getTenantConfiguration(sourceAddress.getTenantId(), commandContext.getTracingContext());
tenantTracker.compose(tenantObject -> {
if (!command.isValid()) {
return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "malformed command message"));
}
if (!HonoProtonHelper.isLinkOpenAndConnected(sender)) {
return Future.failedFuture(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "sender link is not open"));
}
return checkMessageLimit(tenantObject, command.getPayloadSize(), commandContext.getTracingContext());
}).compose(success -> {
// check the via-gateways, ensuring that the gateway may act on behalf of the device at this point in time
if (authenticatedDevice != null && !authenticatedDevice.getDeviceId().equals(sourceAddress.getResourceId())) {
return getRegistrationAssertion(authenticatedDevice.getTenantId(), sourceAddress.getResourceId(), authenticatedDevice, commandContext.getTracingContext());
}
return Future.succeededFuture();
}).compose(success -> {
onCommandReceived(tenantTracker.result(), sender, commandContext);
return Future.succeededFuture();
}).otherwise(failure -> {
if (failure instanceof ClientErrorException) {
commandContext.reject(failure);
} else {
commandContext.release(failure);
}
metrics.reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, sourceAddress.getTenantId(), tenantTracker.result(), ProcessingOutcome.from(failure), command.getPayloadSize(), timer);
return null;
});
};
final Future<RegistrationAssertion> tokenTracker = Optional.ofNullable(authenticatedDevice).map(v -> getRegistrationAssertion(authenticatedDevice.getTenantId(), sourceAddress.getResourceId(), authenticatedDevice, span.context())).orElseGet(Future::succeededFuture);
if (authenticatedDevice != null && !authenticatedDevice.getDeviceId().equals(sourceAddress.getResourceId())) {
// gateway scenario
return tokenTracker.compose(v -> getCommandConsumerFactory().createCommandConsumer(sourceAddress.getTenantId(), sourceAddress.getResourceId(), authenticatedDevice.getDeviceId(), commandHandler, null, span.context()));
} else {
return tokenTracker.compose(v -> getCommandConsumerFactory().createCommandConsumer(sourceAddress.getTenantId(), sourceAddress.getResourceId(), commandHandler, null, span.context()));
}
}
use of org.eclipse.hono.client.command.CommandContext 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.CommandContext in project hono by eclipse.
the class LoraProtocolAdapter method handleCommand.
private void handleCommand(final CommandContext commandContext) {
Tags.COMPONENT.set(commandContext.getTracingSpan(), getTypeName());
final Sample timer = metrics.startTimer();
final Command command = commandContext.getCommand();
if (command.getGatewayId() == null) {
final String errorMsg = "no gateway defined for command";
LOG.debug("{} [{}]", errorMsg, command);
commandContext.release(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, errorMsg));
return;
}
final String tenant = command.getTenant();
final String gatewayId = command.getGatewayId();
final LoraProvider loraProvider = Optional.ofNullable(commandSubscriptions.get(new SubscriptionKey(tenant, gatewayId))).map(Pair::two).orElse(null);
if (loraProvider == null) {
LOG.debug("received command for unknown gateway [{}] for tenant [{}]", gatewayId, tenant);
TracingHelper.logError(commandContext.getTracingSpan(), String.format("received command for unknown gateway [%s]", gatewayId));
commandContext.release(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "received command for unknown gateway"));
return;
}
final Future<TenantObject> tenantTracker = getTenantConfiguration(tenant, commandContext.getTracingContext());
tenantTracker.compose(tenantObject -> {
if (command.isValid()) {
return checkMessageLimit(tenantObject, command.getPayloadSize(), commandContext.getTracingContext());
} else {
return Future.failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "malformed command message"));
}
}).compose(success -> getRegistrationClient().assertRegistration(tenant, gatewayId, null, commandContext.getTracingContext())).compose(registrationAssertion -> sendCommandToGateway(commandContext, loraProvider, registrationAssertion.getCommandEndpoint())).onSuccess(aVoid -> {
addMicrometerSample(commandContext, timer);
commandContext.accept();
metrics.reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenant, tenantTracker.result(), MetricsTags.ProcessingOutcome.FORWARDED, command.getPayloadSize(), timer);
}).onFailure(t -> {
LOG.debug("error sending command", t);
commandContext.release(t);
metrics.reportCommand(command.isOneWay() ? Direction.ONE_WAY : Direction.REQUEST, tenant, tenantTracker.result(), MetricsTags.ProcessingOutcome.from(t), command.getPayloadSize(), timer);
});
}
use of org.eclipse.hono.client.command.CommandContext 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);
}
Aggregations