Add Dead Letter Queue support to the consumer pipeline - #351
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
There was a problem hiding this comment.
Pull request overview
This PR adds dead letter queue (DLQ) support to the Dafda Kafka consumer pipeline so failed messages (after optional retries) can be forwarded to a Kafka dead-letter topic and the consumer can commit the offset and continue.
Changes:
- Introduces
IDeadLetterQueuewith implementations for Kafka (KafkaDeadLetterQueue) and a no-op default (NullDeadLetterQueue). - Extends consumption flow to retry handler dispatch and publish failures to DLQ before committing.
- Adds fluent configuration (
WithDeadLetterQueue,WithMaxRetries) and unit tests for topic resolution and DLQ behavior.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Dafda/Consuming/NullDeadLetterQueue.cs | Adds a no-op DLQ implementation used when DLQ is not configured. |
| src/Dafda/Consuming/MessageResult.cs | Adds RawMessage to support forwarding the original Kafka payload to a DLQ. |
| src/Dafda/Consuming/KafkaDeadLetterQueue.cs | Implements publishing failed messages to a Kafka dead-letter topic with diagnostic headers. |
| src/Dafda/Consuming/KafkaConsumerScope.cs | Captures RawMessage and wires message metadata needed for DLQ publishing. |
| src/Dafda/Consuming/IDeadLetterQueue.cs | Defines the internal DLQ abstraction for failed message forwarding. |
| src/Dafda/Consuming/Consumer.cs | Adds retry + DLQ dispatch logic around handler execution. |
| src/Dafda/Configuration/DeadLetterQueueOptions.cs | Adds fluent DLQ configuration options including max retries validation. |
| src/Dafda/Configuration/ConsumerServiceCollectionExtensions.cs | Wires DLQ factory + max retries into the hosted consumer registration. |
| src/Dafda/Configuration/ConsumerOptions.cs | Exposes WithDeadLetterQueue(...) on the public consumer configuration surface. |
| src/Dafda/Configuration/ConsumerConfigurationBuilder.cs | Builds DLQ factory and propagates max retries into built configuration. |
| src/Dafda/Configuration/ConsumerConfiguration.cs | Stores DLQ factory and max retries in the built consumer configuration. |
| src/Dafda.Tests/Consuming/TestKafkaDeadLetterQueue.cs | Adds tests for DLQ topic name resolution behavior. |
| src/Dafda.Tests/Consuming/TestConsumer.cs | Adds tests for retry/DLQ behavior and cancellation behavior during dispatch. |
| src/Dafda.Tests/Builders/ConsumerBuilder.cs | Updates test builder to support injecting DLQ and max retries. |
Suppressed comments (1)
src/Dafda/Configuration/ConsumerServiceCollectionExtensions.cs:86
- Same as above: the dead letter queue is created via the factory but never disposed (KafkaDeadLetterQueue implements IDisposable and owns an IProducer). Registering a shutdown callback here would avoid leaking the producer for the lifetime of the host.
configuration.EnableAutoCommit,
configuration.DeadLetterQueueFactory(provider),
configuration.MaxRetries
),
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
Suppressed comments (1)
src/Dafda.Tests/Consuming/TestConsumer.cs:11
using TestDoubles;is inside theDafda.Tests.Consumingnamespace scope, so it will look forDafda.Tests.Consuming.TestDoubles, which doesn’t exist (test doubles are underDafda.Tests.TestDoubles). Use the fully-qualified namespace.
using TestDoubles;
Pull request overview
This PR adds dead letter queue (DLQ) support to the Dafda Kafka consumer pipeline so failed messages (after optional retries) can be forwarded to a Kafka dead-letter topic and the consumer can commit the offset and continue.
Why
Today a single "poison" message can take down or stall a consumer. When a message handler throws, the offset is never committed, so Dafda has only two outcomes:
ConsumerHostedService, and the default failure strategy callsStopApplication(). One bad message stops the whole service.RestartConsumer, the consumer restarts, re-reads from the same uncommitted offset, hits the same message, throws again… an infinite reprocessing loop that blocks every message behind it.Neither is acceptable for a message that will never succeed (bad payload, unhandled edge case, downstream contract change). This PR adds a dead letter queue so poison messages are parked on a separate topic and the consumer moves on.
Changes
IDeadLetterQueuewith a Kafka implementation (KafkaDeadLetterQueue) and a no-op default (NullDeadLetterQueue).WithDeadLetterQueue(...)andWithMaxRetries(...)— plus unit tests for topic resolution and DLQ behavior.Usage
The topic name is optional. When omitted, it's derived per message as
"{topic}.{groupId}.dead-letter"(e.g.orders.order-processor.dead-letter). Including the consumer group scopes the DLQ to the group that actually failed — important because a message that's poison for one consumer may be perfectly valid for another consuming the same topic.Behavior
The dead-lettered record preserves the original key and raw payload, with diagnostics added as Kafka headers (source topic, exception type/message, timestamp).
Design notes
WithDeadLetterQueue(...), the consumer usesNullDeadLetterQueueand exceptions propagate exactly as before. No existing option is affected.WithConsumerErrorHandler. The DLQ absorbs per-message handler failures; genuinely catastrophic errors (connection loss, commit failures, and DLQ publish failures) still flow to the configured error handler.KafkaDeadLetterQueueowns a producer and is disposed through theConsumerHostedService → Consumerlifecycle on host shutdown.Tests
Added coverage for retry-then-dead-letter, retry-then-succeed (no DLQ), propagation when DLQ is disabled, cancellation, disposal, and topic-name resolution (explicit, derived with group id, and group-less fallback).