use of org.eclipse.hono.util.TimeUntilDisconnectNotification in project hono by eclipse.
the class DownstreamMessage method getTimeUntilDisconnectNotification.
/**
* Returns the time until disconnection notification of this downstream message.
*
* @return A notification if the message contains a TTD value {@link Optional#empty()} otherwise.
*/
default Optional<TimeUntilDisconnectNotification> getTimeUntilDisconnectNotification() {
final Integer ttd = getTimeTillDisconnect();
final Instant creationTime = getCreationTime();
if (ttd == null) {
return Optional.empty();
} else if (ttd == 0 || TimeUntilDisconnectNotification.isDeviceCurrentlyConnected(ttd, creationTime != null ? creationTime.toEpochMilli() : null)) {
final String tenantId = getTenantId();
final String deviceId = getDeviceId();
if (tenantId != null && deviceId != null) {
final TimeUntilDisconnectNotification notification = new TimeUntilDisconnectNotification(tenantId, deviceId, ttd, creationTime);
return Optional.of(notification);
}
}
return Optional.empty();
}
use of org.eclipse.hono.util.TimeUntilDisconnectNotification in project hono by eclipse.
the class CommandAndControlMqttIT method testSendCommandFailsForCommandNotAcknowledgedByDevice.
/**
* Verifies that the adapter forwards the <em>released</em> disposition back to the
* application if the device hasn't sent an acknowledgement for the command message
* published to the device.
*
* @param endpointConfig The endpoints to use for sending/receiving commands.
* @param ctx The vert.x test context.
* @throws InterruptedException if not all commands and responses are exchanged in time.
*/
@ParameterizedTest(name = IntegrationTestSupport.PARAMETERIZED_TEST_NAME_PATTERN)
@MethodSource("allCombinations")
@Timeout(timeUnit = TimeUnit.SECONDS, value = 20)
public void testSendCommandFailsForCommandNotAcknowledgedByDevice(final MqttCommandEndpointConfiguration endpointConfig, final VertxTestContext ctx) throws InterruptedException {
final MqttQoS subscribeQos = MqttQoS.AT_LEAST_ONCE;
final VertxTestContext setup = new VertxTestContext();
final Checkpoint ready = setup.checkpoint(2);
final String commandTargetDeviceId = endpointConfig.isSubscribeAsGateway() ? helper.setupGatewayDeviceBlocking(tenantId, deviceId, 5) : deviceId;
final int totalNoOfCommandsToSend = 2;
final CountDownLatch commandsFailed = new CountDownLatch(totalNoOfCommandsToSend);
final AtomicInteger receivedMessagesCounter = new AtomicInteger(0);
final AtomicInteger counter = new AtomicInteger();
final Handler<MqttPublishMessage> commandConsumer = msg -> {
LOGGER.trace("received command [{}] - no response sent here", msg.topicName());
final ResourceIdentifier topic = ResourceIdentifier.fromString(msg.topicName());
ctx.verify(() -> {
endpointConfig.assertCommandPublishTopicStructure(topic, commandTargetDeviceId, false, "setValue");
});
receivedMessagesCounter.incrementAndGet();
};
final Function<Buffer, Future<Void>> commandSender = payload -> {
counter.incrementAndGet();
return helper.sendCommand(tenantId, commandTargetDeviceId, "setValue", "text/plain", payload, helper.getSendCommandTimeout(counter.get() == 1)).mapEmpty();
};
helper.registry.addDeviceToTenant(tenantId, deviceId, password).compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password)).compose(ok -> injectMqttClientPubAckBlocker(new AtomicBoolean(true))).compose(ok -> createConsumer(tenantId, msg -> {
// expect empty notification with TTD -1
setup.verify(() -> assertThat(msg.getContentType()).isEqualTo(EventConstants.CONTENT_TYPE_EMPTY_NOTIFICATION));
final TimeUntilDisconnectNotification notification = msg.getTimeUntilDisconnectNotification().orElse(null);
LOGGER.info("received notification [{}]", notification);
setup.verify(() -> assertThat(notification).isNotNull());
if (notification.getTtd() == -1) {
ready.flag();
}
})).compose(conAck -> subscribeToCommands(commandTargetDeviceId, commandConsumer, endpointConfig, subscribeQos)).onComplete(setup.succeeding(ok -> ready.flag()));
assertWithMessage("setup of adapter finished within %s seconds", IntegrationTestSupport.getTestSetupTimeout()).that(setup.awaitCompletion(IntegrationTestSupport.getTestSetupTimeout(), TimeUnit.SECONDS)).isTrue();
if (setup.failed()) {
ctx.failNow(setup.causeOfFailure());
return;
}
final AtomicInteger commandsSent = new AtomicInteger(0);
final AtomicLong lastReceivedTimestamp = new AtomicLong(0);
final long start = System.currentTimeMillis();
while (commandsSent.get() < totalNoOfCommandsToSend) {
final CountDownLatch commandSent = new CountDownLatch(1);
context.runOnContext(go -> {
final Buffer msg = Buffer.buffer("value: " + commandsSent.getAndIncrement());
commandSender.apply(msg).onComplete(sendAttempt -> {
if (sendAttempt.succeeded()) {
LOGGER.debug("sending command {} succeeded unexpectedly", commandsSent.get());
} else {
if (sendAttempt.cause() instanceof ServerErrorException && ((ServerErrorException) sendAttempt.cause()).getErrorCode() == HttpURLConnection.HTTP_UNAVAILABLE && !(sendAttempt.cause() instanceof SendMessageTimeoutException)) {
LOGGER.debug("sending command {} failed as expected: {}", commandsSent.get(), sendAttempt.cause().toString());
lastReceivedTimestamp.set(System.currentTimeMillis());
commandsFailed.countDown();
if (commandsFailed.getCount() % 20 == 0) {
LOGGER.info("commands failed as expected: {}", totalNoOfCommandsToSend - commandsFailed.getCount());
}
} else {
LOGGER.debug("sending command {} failed with an unexpected error", commandsSent.get(), sendAttempt.cause());
}
}
commandSent.countDown();
});
});
commandSent.await();
}
// have to wait an extra MqttAdapterProperties.DEFAULT_COMMAND_ACK_TIMEOUT (100ms) for each command message
final long timeToWait = totalNoOfCommandsToSend * 300;
if (!commandsFailed.await(timeToWait, TimeUnit.MILLISECONDS)) {
LOGGER.info("Timeout of {} milliseconds reached, stop waiting for commands", timeToWait);
}
assertThat(receivedMessagesCounter.get()).isEqualTo(totalNoOfCommandsToSend);
final long commandsCompleted = totalNoOfCommandsToSend - commandsFailed.getCount();
LOGGER.info("commands sent: {}, commands failed: {} after {} milliseconds", commandsSent.get(), commandsCompleted, lastReceivedTimestamp.get() - start);
if (commandsCompleted == commandsSent.get()) {
ctx.completeNow();
} else {
ctx.failNow(new java.lang.IllegalStateException("did not complete all commands sent"));
}
}
use of org.eclipse.hono.util.TimeUntilDisconnectNotification in project hono by eclipse.
the class CommandAndControlMqttIT method testSendCommandViaAmqpFailsForMalformedMessage.
/**
* Verifies that the adapter rejects malformed command messages sent by applications.
* <p>
* This test is applicable only if the messaging network type is AMQP.
*
* @param endpointConfig The endpoints to use for sending/receiving commands.
* @param ctx The vert.x test context.
* @throws InterruptedException if not all commands and responses are exchanged in time.
*/
@ParameterizedTest(name = IntegrationTestSupport.PARAMETERIZED_TEST_NAME_PATTERN)
@MethodSource("allCombinations")
@Timeout(timeUnit = TimeUnit.SECONDS, value = 20)
@AssumeMessagingSystem(type = MessagingType.amqp)
public void testSendCommandViaAmqpFailsForMalformedMessage(final MqttCommandEndpointConfiguration endpointConfig, final VertxTestContext ctx) throws InterruptedException {
final String commandTargetDeviceId = endpointConfig.isSubscribeAsGateway() ? helper.setupGatewayDeviceBlocking(tenantId, deviceId, 5) : deviceId;
final AtomicReference<GenericSenderLink> amqpCmdSenderRef = new AtomicReference<>();
final String linkTargetAddress = endpointConfig.getSenderLinkTargetAddress(tenantId);
final VertxTestContext setup = new VertxTestContext();
final Checkpoint ready = setup.checkpoint(2);
createConsumer(tenantId, msg -> {
// expect empty notification with TTD -1
setup.verify(() -> assertThat(msg.getContentType()).isEqualTo(EventConstants.CONTENT_TYPE_EMPTY_NOTIFICATION));
final TimeUntilDisconnectNotification notification = msg.getTimeUntilDisconnectNotification().orElse(null);
LOGGER.info("received notification [{}]", notification);
if (notification.getTtd() == -1) {
ready.flag();
}
}).compose(consumer -> helper.registry.addDeviceToTenant(tenantId, deviceId, password)).compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password)).compose(conAck -> subscribeToCommands(commandTargetDeviceId, msg -> {
// all commands should get rejected because they fail to pass the validity check
ctx.failNow(new IllegalStateException("should not have received command"));
}, endpointConfig, MqttQoS.AT_MOST_ONCE)).compose(ok -> helper.createGenericAmqpMessageSender(endpointConfig.getNorthboundEndpoint(), tenantId)).onComplete(setup.succeeding(genericSender -> {
LOGGER.debug("created generic sender for sending commands [target address: {}]", linkTargetAddress);
amqpCmdSenderRef.set(genericSender);
ready.flag();
}));
assertWithMessage("setup of adapter finished within %s seconds", IntegrationTestSupport.getTestSetupTimeout()).that(setup.awaitCompletion(IntegrationTestSupport.getTestSetupTimeout(), TimeUnit.SECONDS)).isTrue();
if (setup.failed()) {
ctx.failNow(setup.causeOfFailure());
return;
}
final Checkpoint failedAttempts = ctx.checkpoint(2);
final String messageAddress = endpointConfig.getCommandMessageAddress(tenantId, commandTargetDeviceId);
LOGGER.debug("sending command message lacking subject");
final Message messageWithoutSubject = ProtonHelper.message("input data");
messageWithoutSubject.setAddress(messageAddress);
messageWithoutSubject.setMessageId("message-id");
messageWithoutSubject.setReplyTo("reply/to/address");
amqpCmdSenderRef.get().sendAndWaitForOutcome(messageWithoutSubject, NoopSpan.INSTANCE).onComplete(ctx.failing(t -> {
ctx.verify(() -> assertThat(t).isInstanceOf(ClientErrorException.class));
failedAttempts.flag();
}));
LOGGER.debug("sending command message lacking message ID and correlation ID");
final Message messageWithoutId = ProtonHelper.message("input data");
messageWithoutId.setAddress(messageAddress);
messageWithoutId.setSubject("setValue");
messageWithoutId.setReplyTo("reply/to/address");
amqpCmdSenderRef.get().sendAndWaitForOutcome(messageWithoutId, NoopSpan.INSTANCE).onComplete(ctx.failing(t -> {
ctx.verify(() -> assertThat(t).isInstanceOf(ClientErrorException.class));
failedAttempts.flag();
}));
}
use of org.eclipse.hono.util.TimeUntilDisconnectNotification in project hono by eclipse.
the class CommandAndControlAmqpIT method testSendCommandFailsWhenNoCredit.
/**
* Verifies that the adapter immediately forwards the <em>released</em> disposition
* if there is no credit left for sending the command to the device.
* <p>
* If Kafka is used, this means a corresponding error command response is published.
*
* @param endpointConfig The endpoints to use for sending/receiving commands.
* @param ctx The vert.x test context.
* @throws InterruptedException if not all commands and responses are exchanged in time.
*/
@ParameterizedTest(name = IntegrationTestSupport.PARAMETERIZED_TEST_NAME_PATTERN)
@MethodSource("allCombinations")
@Timeout(timeUnit = TimeUnit.SECONDS, value = 10)
public void testSendCommandFailsWhenNoCredit(final AmqpCommandEndpointConfiguration endpointConfig, final VertxTestContext ctx) throws InterruptedException {
final String commandTargetDeviceId = endpointConfig.isSubscribeAsGateway() ? helper.setupGatewayDeviceBlocking(tenantId, deviceId, 5) : deviceId;
final String firstCommandSubject = "firstCommandSubject";
final Promise<Void> firstCommandReceived = Promise.promise();
final VertxTestContext setup = new VertxTestContext();
final Checkpoint setupDone = setup.checkpoint();
final Checkpoint preconditions = setup.checkpoint(1);
connectToAdapter(tenantId, deviceId, password, () -> createEventConsumer(tenantId, msg -> {
// expect empty notification with TTD -1
setup.verify(() -> assertThat(msg.getContentType()).isEqualTo(EventConstants.CONTENT_TYPE_EMPTY_NOTIFICATION));
final TimeUntilDisconnectNotification notification = msg.getTimeUntilDisconnectNotification().orElse(null);
log.info("received notification [{}]", notification);
setup.verify(() -> assertThat(notification).isNotNull());
if (notification.getTtd() == -1) {
preconditions.flag();
}
})).compose(con -> subscribeToCommands(endpointConfig, tenantId, commandTargetDeviceId)).onSuccess(recv -> {
recv.handler((delivery, msg) -> {
log.info("received command [name: {}, reply-to: {}, correlation-id: {}]", msg.getSubject(), msg.getReplyTo(), msg.getCorrelationId());
ctx.verify(() -> {
assertThat(msg.getSubject()).isEqualTo(firstCommandSubject);
});
firstCommandReceived.complete();
ProtonHelper.accepted(delivery, true);
// don't send credits
});
// just give 1 initial credit
recv.flow(1);
}).onComplete(setup.succeeding(v -> setupDone.flag()));
assertWithMessage("setup of adapter finished within %s seconds", IntegrationTestSupport.getTestSetupTimeout()).that(setup.awaitCompletion(IntegrationTestSupport.getTestSetupTimeout(), TimeUnit.SECONDS)).isTrue();
if (setup.failed()) {
ctx.failNow(setup.causeOfFailure());
return;
}
// send first command
helper.sendOneWayCommand(tenantId, commandTargetDeviceId, firstCommandSubject, "text/plain", Buffer.buffer("cmd"), helper.getSendCommandTimeout(true)).onFailure(ctx::failNow).compose(ok -> {
log.info("sent first command [subject: {}]", firstCommandSubject);
return firstCommandReceived.future();
}).compose(ok -> helper.sendCommand(tenantId, commandTargetDeviceId, "secondCommandSubject", "text/plain", Buffer.buffer("cmd"), helper.getSendCommandTimeout(false))).onComplete(ctx.failing(t -> {
ctx.verify(() -> {
assertThat(t).isInstanceOf(ServerErrorException.class);
assertThat(((ServerErrorException) t).getErrorCode()).isEqualTo(HttpURLConnection.HTTP_UNAVAILABLE);
// with no explicit credit check, the AMQP adapter would just run into the
// "waiting for delivery update" timeout (after 1s) and the error here would be caused
// by a request timeout in the sendOneWayCommand() method above
assertThat(t).isNotInstanceOf(SendMessageTimeoutException.class);
assertThat(t.getMessage()).doesNotContain("timed out");
});
ctx.completeNow();
}));
}
use of org.eclipse.hono.util.TimeUntilDisconnectNotification in project hono by eclipse.
the class HonoExampleApplicationBase method handlePermanentlyConnectedCommandReadinessNotification.
/**
* Handle a ttd notification for permanently connected devices.
* <p>
* Instead of immediately handling the notification, it is first put to a map and a timer is started to handle it
* later. Notifications for the same device that are received before the timer expired, will overwrite the original
* notification. By this an <em>event flickering</em> (like it could occur when starting the app while several
* notifications were persisted in the messaging network) is handled correctly.
* <p>
* If the contained <em>ttd</em> is set to -1, a command will be sent periodically every
* {@link HonoExampleConstants#COMMAND_INTERVAL_FOR_DEVICES_CONNECTED_WITH_UNLIMITED_EXPIRY} seconds to the device
* until a new notification was received with a <em>ttd</em> set to 0.
*
* @param notification The notification of a permanently connected device to handle.
*/
private void handlePermanentlyConnectedCommandReadinessNotification(final TimeUntilDisconnectNotification notification) {
final String keyForDevice = notification.getTenantAndDeviceId();
final TimeUntilDisconnectNotification previousNotification = pendingTtdNotification.get(keyForDevice);
if (previousNotification != null) {
if (notification.getCreationTime().isAfter(previousNotification.getCreationTime())) {
LOG.info("Set new ttd value [{}] of notification for [{}]", notification.getTtd(), notification.getTenantAndDeviceId());
pendingTtdNotification.put(keyForDevice, notification);
} else {
LOG.trace("Received notification for [{}] that was already superseded by newer [{}]", notification, previousNotification);
}
} else {
pendingTtdNotification.put(keyForDevice, notification);
// there was no notification available already, so start a handler now
vertx.setTimer(1000, timerId -> {
LOG.debug("Handle device notification for [{}].", notification.getTenantAndDeviceId());
// now take the notification from the pending map and handle it
final TimeUntilDisconnectNotification notificationToHandle = pendingTtdNotification.remove(keyForDevice);
if (notificationToHandle != null) {
if (notificationToHandle.getTtd() == -1) {
LOG.info("Device notified as being ready to receive a command until further notice : [{}].", notificationToHandle);
// cancel a still existing timer for this device (if found)
cancelPeriodicCommandSender(notification);
// immediately send the first command
sendCommand(notificationToHandle);
// for devices that stay connected, start a periodic timer now that repeatedly sends a command
// to the device
vertx.setPeriodic((long) HonoExampleConstants.COMMAND_INTERVAL_FOR_DEVICES_CONNECTED_WITH_UNLIMITED_EXPIRY * 1000, id -> {
sendCommand(notificationToHandle);
// register a canceler for this timer directly after it was created
setPeriodicCommandSenderTimerCanceler(id, notification);
});
} else {
LOG.info("Device notified as not being ready to receive a command (anymore) : [{}].", notification);
cancelPeriodicCommandSender(notificationToHandle);
LOG.debug("Device will not receive further commands : [{}].", notification.getTenantAndDeviceId());
}
}
});
}
}
Aggregations