use of org.eclipse.hono.client.command.Command in project hono by eclipse.
the class ProtocolAdapterMockSupport method givenARequestResponseCommandContext.
/**
* Creates a mocked context for a request-response command.
*
* @param tenantId The tenant that the command's target device belongs to.
* @param deviceId The target device's identifier.
* @param name The command name.
* @param contentType The type of the payload or {@code null} if the command has no payload.
* @param payload The command's payload.
* @param replyToId The reply-to-id to use.
* @param messagingType The type of messaging system to use.
* @return The mocked context.
*/
protected CommandContext givenARequestResponseCommandContext(final String tenantId, final String deviceId, final String name, final String replyToId, final String contentType, final Buffer payload, final MessagingType messagingType) {
final Command command = newRequestResponseCommand(tenantId, deviceId, name, replyToId, contentType, payload, messagingType);
final CommandContext context = mock(CommandContext.class);
when(context.getCommand()).thenReturn(command);
when(context.getTracingSpan()).thenReturn(NoopSpan.INSTANCE);
return context;
}
use of org.eclipse.hono.client.command.Command in project hono by eclipse.
the class ProtocolAdapterMockSupport method newOneWayCommand.
private Command newOneWayCommand(final String tenantId, final String deviceId, final String name, final String contentType, final Buffer payload) {
final Command command = mock(Command.class);
when(command.getTenant()).thenReturn(tenantId);
when(command.getGatewayOrDeviceId()).thenReturn(deviceId);
when(command.getDeviceId()).thenReturn(deviceId);
when(command.getName()).thenReturn(name);
when(command.getContentType()).thenReturn(contentType);
when(command.getPayload()).thenReturn(payload);
when(command.getPayloadSize()).thenReturn(Optional.ofNullable(payload).map(Buffer::length).orElse(0));
when(command.isOneWay()).thenReturn(true);
when(command.isValid()).thenReturn(true);
return command;
}
use of org.eclipse.hono.client.command.Command 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.Command 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.Command 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);
});
}
}
Aggregations