use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class KafkaBasedInternalCommandConsumer method handleCommandMessage.
void handleCommandMessage(final KafkaConsumerRecord<String, Buffer> record) {
// get partition/offset of the command record - related to the tenant-based topic the command was originally received in
final Integer commandPartition = KafkaRecordHelper.getOriginalPartitionHeader(record.headers()).orElse(null);
final Long commandOffset = KafkaRecordHelper.getOriginalOffsetHeader(record.headers()).orElse(null);
if (commandPartition == null || commandOffset == null) {
LOG.warn("command record is invalid - missing required original partition/offset headers");
return;
}
final KafkaBasedCommand command;
try {
command = KafkaBasedCommand.fromRoutedCommandRecord(record);
} catch (final IllegalArgumentException e) {
LOG.warn("command record is invalid [tenant-id: {}, device-id: {}]", KafkaRecordHelper.getTenantId(record.headers()).orElse(null), KafkaRecordHelper.getDeviceId(record.headers()).orElse(null), e);
return;
}
// check whether command has already been received and handled;
// partition index and offset here are related to the *tenant-based* topic the command was originally received in
// therefore they are stored in a map with the tenant as key
final Map<Integer, Long> lastHandledPartitionOffsets = lastHandledPartitionOffsetsPerTenant.computeIfAbsent(command.getTenant(), k -> new HashMap<>());
final Long lastHandledOffset = lastHandledPartitionOffsets.get(commandPartition);
if (lastHandledOffset != null && commandOffset <= lastHandledOffset) {
if (LOG.isDebugEnabled()) {
LOG.debug("ignoring command - record partition offset {} <= last handled offset {} [{}]", commandOffset, lastHandledOffset, command);
}
} else {
lastHandledPartitionOffsets.put(commandPartition, commandOffset);
final CommandHandlerWrapper commandHandler = commandHandlers.getCommandHandler(command.getTenant(), command.getGatewayOrDeviceId());
if (commandHandler != null && commandHandler.getGatewayId() != null) {
// Gateway information set in command handler means a gateway has subscribed for commands for a specific device.
// This information isn't getting set in the record (by the Command Router) and therefore has to be adopted manually here.
command.setGatewayId(commandHandler.getGatewayId());
}
final SpanContext spanContext = KafkaTracingHelper.extractSpanContext(tracer, record);
final SpanContext followsFromSpanContext = commandHandler != null ? commandHandler.getConsumerCreationSpanContext() : null;
final Span currentSpan = CommandContext.createSpan(tracer, command, spanContext, followsFromSpanContext, getClass().getSimpleName());
currentSpan.setTag(MessageHelper.APP_PROPERTY_ADAPTER_INSTANCE_ID, adapterInstanceId);
KafkaTracingHelper.TAG_OFFSET.set(currentSpan, record.offset());
final var commandContext = new KafkaBasedCommandContext(command, commandResponseSender, currentSpan);
tenantClient.get(command.getTenant(), spanContext).onFailure(t -> {
if (ServiceInvocationException.extractStatusCode(t) == HttpURLConnection.HTTP_NOT_FOUND) {
commandContext.reject(new TenantDisabledOrNotRegisteredException(command.getTenant(), HttpURLConnection.HTTP_NOT_FOUND));
} else {
commandContext.release(new ServerErrorException(command.getTenant(), HttpURLConnection.HTTP_UNAVAILABLE, "error retrieving tenant configuration", t));
}
}).onSuccess(tenantConfig -> {
commandContext.put(CommandContext.KEY_TENANT_CONFIG, tenantConfig);
if (commandHandler != null) {
LOG.trace("using [{}] for received command [{}]", commandHandler, command);
// command.isValid() check not done here - it is to be done in the command handler
commandHandler.handleCommand(commandContext);
} else {
LOG.info("no command handler found for command [{}]", command);
commandContext.release(new NoConsumerException("no command handler found for command"));
}
});
}
}
use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class KafkaBasedInternalCommandSender method sendCommand.
@Override
public Future<Void> sendCommand(final CommandContext commandContext, final String adapterInstanceId) {
Objects.requireNonNull(commandContext);
Objects.requireNonNull(adapterInstanceId);
final Command command = commandContext.getCommand();
if (!(command instanceof KafkaBasedCommand)) {
commandContext.release();
log.error("command is not an instance of KafkaBasedCommand");
throw new IllegalArgumentException("command is not an instance of KafkaBasedCommand");
}
final String topicName = getInternalCommandTopic(adapterInstanceId);
final Span currentSpan = startChildSpan("delegate Command request", topicName, command.getTenant(), command.getDeviceId(), commandContext.getTracingContext());
return sendAndWaitForOutcome(topicName, command.getTenant(), command.getDeviceId(), command.getPayload(), getHeaders((KafkaBasedCommand) command), currentSpan).onSuccess(v -> commandContext.accept()).onFailure(thr -> commandContext.release(new ServerErrorException(command.getTenant(), HttpURLConnection.HTTP_UNAVAILABLE, "failed to publish command message on internal command topic", thr))).onComplete(ar -> currentSpan.finish());
}
use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class KafkaBasedInternalCommandConsumerTest method testHandleCommandMessageWithInvalidRecord.
/**
* Verifies that the consumer handles a command record with missing subject by invoking the matching handler.
*/
@Test
void testHandleCommandMessageWithInvalidRecord() {
// command record with missing subject header
final String tenantId = "myTenant";
final String deviceId = "4711";
final List<KafkaHeader> headers = List.of(KafkaRecordHelper.createTenantIdHeader(tenantId), KafkaRecordHelper.createDeviceIdHeader(deviceId), KafkaRecordHelper.createOriginalPartitionHeader(0), KafkaRecordHelper.createOriginalOffsetHeader(0L));
final KafkaConsumerRecord<String, Buffer> commandRecord = getCommandRecord(deviceId, headers);
final Handler<CommandContext> commandHandler = VertxMockSupport.mockHandler();
commandHandlers.putCommandHandler(tenantId, deviceId, null, commandHandler, context);
internalCommandConsumer.handleCommandMessage(commandRecord);
final ArgumentCaptor<CommandContext> commandContextCaptor = ArgumentCaptor.forClass(CommandContext.class);
verify(commandHandler).handle(commandContextCaptor.capture());
assertThat(commandContextCaptor.getValue()).isNotNull();
// assert that command is not valid
assertThat(commandContextCaptor.getValue().getCommand().isValid()).isFalse();
assertThat(commandContextCaptor.getValue().getCommand().getInvalidCommandReason()).contains("subject");
}
use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class KafkaBasedInternalCommandConsumerTest method testHandleCommandMessageWithHandlerForGateway.
/**
* Verifies that the consumer handles a valid message, targeted at a gateway, by invoking the matching command
* handler.
*/
@Test
void testHandleCommandMessageWithHandlerForGateway() {
final String tenantId = "myTenant";
final String deviceId = "4711";
final String gatewayId = "gw-1";
final String subject = "subject";
final List<KafkaHeader> headers = new ArrayList<>(getHeaders(tenantId, deviceId, subject, 0L));
headers.add(KafkaRecordHelper.createViaHeader(gatewayId));
final KafkaConsumerRecord<String, Buffer> commandRecord = getCommandRecord(deviceId, headers);
final Handler<CommandContext> commandHandler = VertxMockSupport.mockHandler();
commandHandlers.putCommandHandler(tenantId, gatewayId, null, commandHandler, context);
internalCommandConsumer.handleCommandMessage(commandRecord);
final ArgumentCaptor<CommandContext> commandContextCaptor = ArgumentCaptor.forClass(CommandContext.class);
verify(commandHandler).handle(commandContextCaptor.capture());
assertThat(commandContextCaptor.getValue()).isNotNull();
assertThat(commandContextCaptor.getValue().getCommand().isValid()).isTrue();
// assert that command is directed at the gateway
assertThat(commandContextCaptor.getValue().getCommand().getGatewayId()).isEqualTo(gatewayId);
assertThat(commandContextCaptor.getValue().getCommand().getDeviceId()).isEqualTo(deviceId);
}
use of org.eclipse.hono.client.command.CommandContext in project hono by eclipse.
the class KafkaBasedInternalCommandConsumerTest method testHandleDuplicateCommandMessage.
/**
* Verifies that the consumer doesn't invoke a matching command handler if the command record
* has a partition offset smaller or equal to one of a command that was already handled.
*/
@Test
void testHandleDuplicateCommandMessage() {
final String tenantId = "myTenant";
final String deviceId = "4711";
final String subject = "subject";
final KafkaConsumerRecord<String, Buffer> commandRecord = getCommandRecord(deviceId, getHeaders(tenantId, deviceId, subject, 10L));
// 2nd command - with smaller offset, indicating it is a duplicate
final KafkaConsumerRecord<String, Buffer> commandRecord2 = getCommandRecord(deviceId, getHeaders(tenantId, deviceId, subject, 5L));
final Handler<CommandContext> commandHandler = VertxMockSupport.mockHandler();
commandHandlers.putCommandHandler(tenantId, deviceId, null, commandHandler, context);
internalCommandConsumer.handleCommandMessage(commandRecord);
internalCommandConsumer.handleCommandMessage(commandRecord2);
final InOrder inOrder = inOrder(commandHandler);
final ArgumentCaptor<CommandContext> commandContextCaptor = ArgumentCaptor.forClass(CommandContext.class);
// first invocation
inOrder.verify(commandHandler).handle(commandContextCaptor.capture());
assertThat(commandContextCaptor.getValue()).isNotNull();
assertThat(commandContextCaptor.getValue().getCommand().isValid()).isTrue();
// verify there was no second invocation
inOrder.verify(commandHandler, never()).handle(any());
}
Aggregations