use of org.apache.qpid.proton.amqp.messaging.Rejected 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.apache.qpid.proton.amqp.messaging.Rejected in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapterTest method testMessageLimitExceededForADownstreamMessage.
private void testMessageLimitExceededForADownstreamMessage(final VertxTestContext ctx, final Message message, final Consumer<Void> postUploadAssertions) {
final ProtonDelivery delivery = mock(ProtonDelivery.class);
// AT LEAST ONCE
when(delivery.remotelySettled()).thenReturn(false);
final AmqpContext amqpContext = AmqpContext.fromMessage(delivery, message, span, null);
// GIVEN an AMQP adapter
givenAnAdapter(properties);
givenATelemetrySenderForAnyTenant();
// which is enabled for a tenant with exceeded message limit
when(resourceLimitChecks.isMessageLimitReached(any(TenantObject.class), anyLong(), any(SpanContext.class))).thenReturn(Future.succeededFuture(Boolean.TRUE));
// WHEN a device uploads a message to the adapter with AT_LEAST_ONCE delivery semantics
adapter.onMessageReceived(amqpContext).onComplete(ctx.failing(t -> {
ctx.verify(() -> {
// THEN the message limit is exceeded
assertThat(((ClientErrorException) t).getErrorCode()).isEqualTo(HttpUtils.HTTP_TOO_MANY_REQUESTS);
// AND the client receives a corresponding REJECTED disposition
verify(delivery).disposition(argThat(s -> {
if (s instanceof Rejected) {
return AmqpError.RESOURCE_LIMIT_EXCEEDED.equals(((Rejected) s).getError().getCondition());
} else {
return false;
}
}), eq(true));
// AND
postUploadAssertions.accept(null);
});
ctx.completeNow();
}));
}
use of org.apache.qpid.proton.amqp.messaging.Rejected in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapterTest method testUploadEventRejectsPresettledMessage.
/**
* Verifies that the adapter rejects presettled messages with an event address.
*
* @param ctx The vert.x test context.
*/
@Test
public void testUploadEventRejectsPresettledMessage(final VertxTestContext ctx) {
// GIVEN an adapter
givenAnAdapter(properties);
givenAnEventSenderForAnyTenant();
// with an enabled tenant
givenAConfiguredTenant(TEST_TENANT_ID, true);
// WHEN a device uploads an event using a presettled message
final Device gateway = new Device(TEST_TENANT_ID, "device");
final ProtonDelivery delivery = mock(ProtonDelivery.class);
// AT MOST ONCE
when(delivery.remotelySettled()).thenReturn(true);
final String to = ResourceIdentifier.fromString(EventConstants.EVENT_ENDPOINT).toString();
final Buffer payload = Buffer.buffer("some payload");
adapter.onMessageReceived(AmqpContext.fromMessage(delivery, getFakeMessage(to, payload), span, gateway)).onComplete(ctx.failing(t -> {
ctx.verify(() -> {
// THEN the adapter does not forward the event
assertNoEventHasBeenSentDownstream();
// AND notifies the device by sending back a REJECTED disposition
verify(delivery).disposition(any(Rejected.class), eq(true));
});
ctx.completeNow();
}));
}
use of org.apache.qpid.proton.amqp.messaging.Rejected in project hono by eclipse.
the class VertxBasedAmqpProtocolAdapterTest method testUploadEventFailsForGatewayOfDifferentTenant.
/**
* Verifies that a request from a gateway to upload an event on behalf of a device that belongs
* to another tenant than the gateway fails.
*
* @param ctx The vert.x test context.
*/
@Test
public void testUploadEventFailsForGatewayOfDifferentTenant(final VertxTestContext ctx) {
// GIVEN an adapter
givenAnAdapter(properties);
givenAnEventSenderForAnyTenant();
// with an enabled tenant
givenAConfiguredTenant(TEST_TENANT_ID, true);
// WHEN a gateway uploads an event on behalf of a device of another tenant
final Device gateway = new Device(TEST_TENANT_ID, "gw");
final ProtonDelivery delivery = mock(ProtonDelivery.class);
// AT LEAST ONCE
when(delivery.remotelySettled()).thenReturn(false);
final String to = ResourceIdentifier.from(EventConstants.EVENT_ENDPOINT, "other-tenant", TEST_DEVICE).toString();
final Buffer payload = Buffer.buffer("some payload");
adapter.onMessageReceived(AmqpContext.fromMessage(delivery, getFakeMessage(to, payload), span, gateway)).onComplete(ctx.failing(t -> {
ctx.verify(() -> {
// THEN the adapter does not send the event
assertNoEventHasBeenSentDownstream();
// AND notifies the device by sending back a REJECTED disposition
verify(delivery).disposition(any(Rejected.class), eq(true));
});
ctx.completeNow();
}));
}
use of org.apache.qpid.proton.amqp.messaging.Rejected in project hono by eclipse.
the class RequestResponseClient method sendRequest.
/**
* Sends a request message via this client's sender link to the peer.
* <p>
* This method first checks if the sender has any credit left. If not, the result handler is failed immediately.
* Otherwise, the request message is sent and a timer is started which fails the result handler,
* if no response is received within <em>requestTimeoutMillis</em> milliseconds.
* <p>
* The given span is never finished by this method.
*
* @param request The message to send.
* @param responseMapper A function mapping a raw AMQP message and a proton delivery to the response type.
* @param currentSpan The <em>Opentracing</em> span used to trace the request execution.
* @return A future indicating the outcome of the operation.
* The future will be failed with a {@link ServerErrorException} if the request cannot be sent to
* the remote service, e.g. because there is no connection to the service or there are no credits
* available for sending the request or the request timed out.
*/
private Future<R> sendRequest(final Message request, final BiFunction<Message, ProtonDelivery, R> responseMapper, final Span currentSpan) {
final String requestTargetAddress = Optional.ofNullable(request.getAddress()).orElse(linkTargetAddress);
Tags.MESSAGE_BUS_DESTINATION.set(currentSpan, requestTargetAddress);
Tags.SPAN_KIND.set(currentSpan, Tags.SPAN_KIND_CLIENT);
Tags.HTTP_METHOD.set(currentSpan, request.getSubject());
if (tenantId != null) {
currentSpan.setTag(MessageHelper.APP_PROPERTY_TENANT_ID, tenantId);
}
return connection.executeOnContext((Promise<R> res) -> {
if (sender.sendQueueFull()) {
LOG.debug("cannot send request to peer, no credit left for link [link target: {}]", linkTargetAddress);
res.fail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "no credit available for sending request"));
sampler.queueFull(tenantId);
} else {
final Map<String, Object> details = new HashMap<>(3);
final Object correlationId = Optional.ofNullable(request.getCorrelationId()).orElse(request.getMessageId());
if (correlationId instanceof String) {
details.put(TracingHelper.TAG_CORRELATION_ID.getKey(), correlationId);
}
details.put(TracingHelper.TAG_CREDIT.getKey(), sender.getCredit());
details.put(TracingHelper.TAG_QOS.getKey(), sender.getQoS().toString());
currentSpan.log(details);
final TriTuple<Promise<R>, BiFunction<Message, ProtonDelivery, R>, Span> handler = TriTuple.of(res, responseMapper, currentSpan);
TracingHelper.injectSpanContext(connection.getTracer(), currentSpan.context(), request);
replyMap.put(correlationId, handler);
final SendMessageSampler.Sample sample = sampler.start(tenantId);
sender.send(request, deliveryUpdated -> {
final Promise<R> failedResult = Promise.promise();
final DeliveryState remoteState = deliveryUpdated.getRemoteState();
sample.completed(remoteState);
if (Rejected.class.isInstance(remoteState)) {
final Rejected rejected = (Rejected) remoteState;
if (rejected.getError() != null) {
LOG.debug("service did not accept request [target address: {}, subject: {}, correlation ID: {}]: {}", requestTargetAddress, request.getSubject(), correlationId, rejected.getError());
failedResult.fail(StatusCodeMapper.fromTransferError(rejected.getError()));
cancelRequest(correlationId, failedResult.future());
} else {
LOG.debug("service did not accept request [target address: {}, subject: {}, correlation ID: {}]", requestTargetAddress, request.getSubject(), correlationId);
failedResult.fail(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST));
cancelRequest(correlationId, failedResult.future());
}
} else if (Accepted.class.isInstance(remoteState)) {
LOG.trace("service has accepted request [target address: {}, subject: {}, correlation ID: {}]", requestTargetAddress, request.getSubject(), correlationId);
currentSpan.log("request accepted by peer");
// if no reply-to is set, the request is assumed to be one-way (no response is expected)
if (request.getReplyTo() == null) {
if (replyMap.remove(correlationId) != null) {
res.complete();
} else {
LOG.trace("accepted request won't be acted upon, request already cancelled [target address: {}, subject: {}, correlation ID: {}]", requestTargetAddress, request.getSubject(), correlationId);
}
}
} else if (Released.class.isInstance(remoteState)) {
LOG.debug("service did not accept request [target address: {}, subject: {}, correlation ID: {}], remote state: {}", requestTargetAddress, request.getSubject(), correlationId, remoteState);
failedResult.fail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE));
cancelRequest(correlationId, failedResult.future());
} else if (Modified.class.isInstance(remoteState)) {
LOG.debug("service did not accept request [target address: {}, subject: {}, correlation ID: {}], remote state: {}", requestTargetAddress, request.getSubject(), correlationId, remoteState);
final Modified modified = (Modified) deliveryUpdated.getRemoteState();
failedResult.fail(modified.getUndeliverableHere() ? new ClientErrorException(HttpURLConnection.HTTP_NOT_FOUND) : new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE));
cancelRequest(correlationId, failedResult.future());
} else if (remoteState == null) {
// possible scenario here: sender link got closed while waiting on the delivery update
final String furtherInfo = !sender.isOpen() ? ", sender link was closed in between" : "";
LOG.warn("got undefined delivery state for service request{} [target address: {}, subject: {}, correlation ID: {}]", furtherInfo, requestTargetAddress, request.getSubject(), correlationId);
failedResult.fail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE));
cancelRequest(correlationId, failedResult.future());
}
});
if (requestTimeoutMillis > 0) {
connection.getVertx().setTimer(requestTimeoutMillis, tid -> {
if (cancelRequest(correlationId, () -> new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "request timed out after " + requestTimeoutMillis + "ms"))) {
sample.timeout();
}
});
}
if (LOG.isDebugEnabled()) {
final String deviceId = MessageHelper.getDeviceId(request);
if (deviceId == null) {
LOG.debug("sent request [target address: {}, subject: {}, correlation ID: {}] to service", requestTargetAddress, request.getSubject(), correlationId);
} else {
LOG.debug("sent request [target address: {}, subject: {}, correlation ID: {}, device ID: {}] to service", requestTargetAddress, request.getSubject(), correlationId, deviceId);
}
}
}
});
}
Aggregations