use of org.eclipse.hono.client.command.kafka.KafkaBasedCommandContext in project hono by eclipse.
the class KafkaCommandProcessingQueue method remove.
/**
* Removes the command represented by the given command context.
* <p>
* To be used for commands for which processing resulted in an error
* and {@link #applySendCommandAction(KafkaBasedCommandContext, Supplier)}
* will not be invoked.
*
* @return {@code true} if the command was removed.
* @param commandContext The context containing the command to remove.
*/
public boolean remove(final KafkaBasedCommandContext commandContext) {
Objects.requireNonNull(commandContext);
final KafkaConsumerRecord<String, Buffer> record = commandContext.getCommand().getRecord();
final TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition());
return Optional.ofNullable(commandQueues.get(topicPartition)).map(commandQueue -> commandQueue.remove(commandContext)).orElse(false);
}
use of org.eclipse.hono.client.command.kafka.KafkaBasedCommandContext in project hono by eclipse.
the class KafkaBasedMappingAndDelegatingCommandHandler method mapAndDelegateIncomingCommandMessage.
/**
* Delegates an incoming command to the protocol adapter instance that the target
* device is connected to.
* <p>
* Determines the target gateway (if applicable) and protocol adapter instance for an incoming command
* and delegates the command to the resulting protocol adapter instance.
*
* @param consumerRecord The consumer record corresponding to the command.
* @return A future indicating the outcome of the operation.
* @throws NullPointerException if any of the parameters is {@code null}.
*/
public Future<Void> mapAndDelegateIncomingCommandMessage(final KafkaConsumerRecord<String, Buffer> consumerRecord) {
Objects.requireNonNull(consumerRecord);
final Timer.Sample timer = getMetrics().startTimer();
final KafkaBasedCommand command;
try {
command = KafkaBasedCommand.from(consumerRecord);
} catch (final IllegalArgumentException exception) {
log.debug("command record is invalid", exception);
return Future.failedFuture("command record is invalid");
}
final SpanContext spanContext = KafkaTracingHelper.extractSpanContext(tracer, consumerRecord);
final Span currentSpan = createSpan(command.getTenant(), command.getDeviceId(), spanContext);
KafkaTracingHelper.setRecordTags(currentSpan, consumerRecord);
final KafkaBasedCommandContext commandContext = new KafkaBasedCommandContext(command, kafkaBasedCommandResponseSender, currentSpan);
command.logToSpan(currentSpan);
if (!command.isValid()) {
log.debug("received invalid command record [{}]", command);
return tenantClient.get(command.getTenant(), currentSpan.context()).compose(tenantConfig -> {
commandContext.put(CommandContext.KEY_TENANT_CONFIG, tenantConfig);
return Future.failedFuture("command is invalid");
}).onComplete(ar -> {
commandContext.reject("malformed command message");
reportInvalidCommand(commandContext, timer);
}).mapEmpty();
}
log.trace("received valid command record [{}]", command);
commandQueue.add(commandContext);
final Promise<Void> resultPromise = Promise.promise();
final long timerId = vertx.setTimer(PROCESSING_TIMEOUT.toMillis(), tid -> {
if (commandQueue.remove(commandContext) || !commandContext.isCompleted()) {
log.info("command processing timed out after {}s [{}]", PROCESSING_TIMEOUT.toSeconds(), commandContext.getCommand());
TracingHelper.logError(commandContext.getTracingSpan(), String.format("command processing timed out after %ds", PROCESSING_TIMEOUT.toSeconds()));
final ServerErrorException error = new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "command processing timed out");
commandContext.release(error);
resultPromise.tryFail(error);
}
});
mapAndDelegateIncomingCommand(commandContext, timer).onComplete(ar -> {
vertx.cancelTimer(timerId);
if (ar.failed()) {
commandQueue.remove(commandContext);
}
Futures.tryHandleResult(resultPromise, ar);
});
return resultPromise.future();
}
use of org.eclipse.hono.client.command.kafka.KafkaBasedCommandContext in project hono by eclipse.
the class KafkaBasedMappingAndDelegatingCommandHandlerTest method testCommandDelegationForValidCommand.
/**
* Verifies the behavior of the
* {@link KafkaBasedMappingAndDelegatingCommandHandler#mapAndDelegateIncomingCommandMessage(KafkaConsumerRecord)}
* method in a scenario with a valid command record.
*/
@Test
public void testCommandDelegationForValidCommand() {
// GIVEN a valid command record
final KafkaConsumerRecord<String, Buffer> commandRecord = getCommandRecord(tenantId, deviceId, "cmd-subject", 0, 0);
// WHEN mapping and delegating the command
cmdHandler.mapAndDelegateIncomingCommandMessage(commandRecord);
final ArgumentCaptor<KafkaBasedCommandContext> commandContextArgumentCaptor = ArgumentCaptor.forClass(KafkaBasedCommandContext.class);
// THEN the message is properly delegated
verify(internalCommandSender, times(1)).sendCommand(commandContextArgumentCaptor.capture(), eq(adapterInstanceId));
final KafkaBasedCommandContext commandContext = commandContextArgumentCaptor.getValue();
assertNotNull(commandContext);
assertTrue(commandContext.getCommand().isValid());
assertEquals(tenantId, commandContext.getCommand().getTenant());
assertEquals(deviceId, commandContext.getCommand().getDeviceId());
assertEquals("cmd-subject", commandContext.getCommand().getName());
}
use of org.eclipse.hono.client.command.kafka.KafkaBasedCommandContext in project hono by eclipse.
the class KafkaBasedMappingAndDelegatingCommandHandlerTest method testCommandDelegationTimesOutWhileCommandGetsSent.
/**
* Verifies the behaviour of the
* {@link KafkaBasedMappingAndDelegatingCommandHandler#mapAndDelegateIncomingCommandMessage(KafkaConsumerRecord)}
* method in a scenario where mapping/delegation of a valid command record times out while the command gets sent.
*/
@Test
public void testCommandDelegationTimesOutWhileCommandGetsSent() {
final AtomicReference<Handler<Long>> timerHandlerRef = new AtomicReference<>();
when(vertx.setTimer(anyLong(), VertxMockSupport.anyHandler())).thenAnswer(invocation -> {
final Handler<Long> handler = invocation.getArgument(1);
timerHandlerRef.set(handler);
return 1L;
});
// GIVEN a valid command record
final KafkaConsumerRecord<String, Buffer> commandRecord = getCommandRecord(tenantId, deviceId, "cmd-subject", 0, 0);
final Promise<Void> sendCommandPromise = Promise.promise();
when(internalCommandSender.sendCommand(any(CommandContext.class), anyString())).thenReturn(sendCommandPromise.future());
// WHEN mapping and delegating the command (with no timeout triggered yet)
final Future<Void> resultFuture = cmdHandler.mapAndDelegateIncomingCommandMessage(commandRecord);
final ArgumentCaptor<KafkaBasedCommandContext> commandContextArgumentCaptor = ArgumentCaptor.forClass(KafkaBasedCommandContext.class);
// THEN the message is properly delegated
verify(internalCommandSender, times(1)).sendCommand(commandContextArgumentCaptor.capture(), eq(adapterInstanceId));
final KafkaBasedCommandContext commandContext = commandContextArgumentCaptor.getValue();
assertNotNull(commandContext);
assertTrue(commandContext.getCommand().isValid());
assertEquals(tenantId, commandContext.getCommand().getTenant());
assertEquals(deviceId, commandContext.getCommand().getDeviceId());
assertEquals("cmd-subject", commandContext.getCommand().getName());
assertThat(resultFuture.isComplete()).isFalse();
// WHEN the timeout is triggered and then sending the command succeeds
timerHandlerRef.get().handle(1L);
sendCommandPromise.complete();
// THEN the result future is failed because the timeout came first
assertThat(resultFuture.failed()).isTrue();
}
use of org.eclipse.hono.client.command.kafka.KafkaBasedCommandContext in project hono by eclipse.
the class KafkaCommandProcessingQueueTest method testSendActionIsInvokedInOrder.
/**
* Verifies that the sendAction is invoked on commands in the queue in the expected order.
*
* @param vertx The vert.x instance to use.
* @param ctx The vert.x test context.
*/
@Test
@SuppressWarnings("rawtypes")
void testSendActionIsInvokedInOrder(final Vertx vertx, final VertxTestContext ctx) {
final Context vertxContext = vertx.getOrCreateContext();
vertxContext.runOnContext(v -> {
final KafkaCommandProcessingQueue kafkaCommandProcessingQueue = new KafkaCommandProcessingQueue(vertxContext);
// GIVEN a number of test commands
final LinkedList<KafkaBasedCommandContext> commandContexts = IntStream.range(0, 5).mapToObj(this::getTestCommandContext).collect(Collectors.toCollection(LinkedList::new));
// ... added to the queue in order
commandContexts.forEach(kafkaCommandProcessingQueue::add);
// WHEN applying the sendAction on these commands in the reverse order
final LinkedList<KafkaBasedCommandContext> sendActionInvoked = new LinkedList<>();
final LinkedList<KafkaBasedCommandContext> applySendActionSucceeded = new LinkedList<>();
final List<Future> resultFutures = new LinkedList<>();
commandContexts.descendingIterator().forEachRemaining(context -> {
resultFutures.add(kafkaCommandProcessingQueue.applySendCommandAction(context, () -> {
sendActionInvoked.add(context);
return Future.succeededFuture();
}).onSuccess(v2 -> applySendActionSucceeded.add(context)));
});
CompositeFuture.all(resultFutures).onComplete(ctx.succeeding(ar -> {
ctx.verify(() -> {
// THEN the commands got sent in the original order
assertThat(sendActionInvoked).isEqualTo(commandContexts);
assertThat(applySendActionSucceeded).isEqualTo(commandContexts);
});
ctx.completeNow();
}));
});
}
Aggregations