Skip to content

fix(impl): Skip ProducerOnSendInterceptor execution when publishing to the local DLQ#234

Merged
edeweerd1A merged 1 commit intoquarkiverse:mainfrom
lmartella1:fix-dlq-skip-interceptors
Mar 13, 2026
Merged

fix(impl): Skip ProducerOnSendInterceptor execution when publishing to the local DLQ#234
edeweerd1A merged 1 commit intoquarkiverse:mainfrom
lmartella1:fix-dlq-skip-interceptors

Conversation

@lmartella1
Copy link
Copy Markdown
Contributor

This PR resolves an inconsistency in DLQ handling where processor failures were sent to the LocalDLQ using the topology-defined Kafka producer, causing ProducerOnSendInterceptor logic to be applied and potentially altering the original poisonous message.

The refactoring introduces a dedicated raw Kafka producer, explicitly marked for DLQ usage, which bypasses interceptor execution. This producer is now shared between DlqDecorator (for processor failures) and LogAndSendToDlqExceptionHandlerDelegate (for deserialization exceptions), ensuring all LocalDLQ records are pushed unmodified except for DLQ metadata headers, and providing consistent DLQ behavior across all failure scenarios.

Fixes: #233

@lmartella1 lmartella1 requested a review from a team as a code owner March 11, 2026 14:04
if (dlqProducer != null) {
dlqProducer.close(GlobalDLQProductionExceptionHandlerDelegate.GRACEFUL_PERIOD);
}
dlqDelegate.configure(configs);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the DlqProducerService is configured 2 times?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LogAndSendToDlqExceptionHandlerDelegate.java implements the DeserializationExceptionHandler interface from Kafka Streams, making it a Configurable class. This means its configure() method is automatically invoked by the Kafka Streams runtime during startup.

As part of refactoring, I moved the DLQ producer setup logic to DlqProducerService.
However, since the Kafka Streams runtime does not call configure() on DlqProducerService, I resolved this by explicitly invoking DlqProducerService.configure() from within LogAndSendToDlqExceptionHandlerDelegate.configure().

So the DlqProducerService is strongly dependent from the LogAndSendToDlqExceptionHandlerDelegate implementation as the latter is the one which configure and initialize the first.

context.forward(record, TopologyProducer.DLQ_SINK_NAME);
// Re-throw so the exception gets logged
metrics.microserviceDlqSentCounter().increment();
throw e;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't less the exception being thrown?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the throw was only meant to log the error (see the comment in the old code "Re-throw so the exception gets logged") I concluded this is no more useful as the DlqProducerService is logging the error as part of the error handling

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not so the TraceDecorator is aware of the error and marks the span in error with the exception as an event?

I hope we have a QuarkusTest testing that flow that makes sure the exception is caught... I think not, we have nothing about tracing here: https://github.com/quarkiverse/quarkus-kafka-streams-processor/blob/main/impl/src/test/java/io/quarkiverse/kafkastreamsprocessor/impl/errors/LogAndSendToDlqExceptionHandlerDelegateQuarkusTest.java nor any check for exception in TracingQuarkusTest.java

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@edeweerd1A indeed, the exception is actually meant to notify the TracerDecorator that the otel span must be failed. I modify the DlqDecorator to throw the exception after the push to the DLQ and I have created all the necessary span checks in the DlqDecoratorQuarkusTest.

Regarding the LogAndSendToDlqExceptionHandler as this one is intercepting Deserialization Exception, the TracerDecorator is not even registered in the message handling. Hence there's no trace at all.

Copy link
Copy Markdown
Contributor

@flazarus1A flazarus1A left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Contributor

@edeweerd1A edeweerd1A left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just have that one doubt

context.forward(record, TopologyProducer.DLQ_SINK_NAME);
// Re-throw so the exception gets logged
metrics.microserviceDlqSentCounter().increment();
throw e;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not so the TraceDecorator is aware of the error and marks the span in error with the exception as an event?

I hope we have a QuarkusTest testing that flow that makes sure the exception is caught... I think not, we have nothing about tracing here: https://github.com/quarkiverse/quarkus-kafka-streams-processor/blob/main/impl/src/test/java/io/quarkiverse/kafkastreamsprocessor/impl/errors/LogAndSendToDlqExceptionHandlerDelegateQuarkusTest.java nor any check for exception in TracingQuarkusTest.java

…o the local DLQ

This PR resolves an inconsistency in DLQ handling where processor failures were sent to the LocalDLQ using the topology-defined Kafka producer, causing ProducerOnSendInterceptor logic to be applied and potentially altering the original poisonous message.

The refactoring introduces a dedicated raw Kafka producer, explicitly marked for DLQ usage, which bypasses interceptor execution.
This producer is now shared between DlqDecorator (for processor failures) and LogAndSendToDlqExceptionHandlerDelegate (for deserialization exceptions), ensuring all LocalDLQ records are pushed unmodified except for DLQ metadata headers, and providing consistent DLQ behavior across all failure scenarios.

Fixes: quarkiverse#233
@lmartella1 lmartella1 force-pushed the fix-dlq-skip-interceptors branch from e7053ee to 5573cfc Compare March 13, 2026 10:06
@edeweerd1A edeweerd1A merged commit ba920ff into quarkiverse:main Mar 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Skip ProducerOnSendInterceptor execution when publishing to the DLQ sink

4 participants