Add the rabbitmq module
This commit is contained in:
112
rabbitmq/docs/04-dead-lettering.md
Normal file
112
rabbitmq/docs/04-dead-lettering.md
Normal file
@@ -0,0 +1,112 @@
|
||||
[← The silent drop](03-the-silent-drop.md) · [Module README](../README.md) · [Acknowledgement →](05-acknowledgement.md)
|
||||
|
||||
# 4. A dead-letter queue that actually works
|
||||
|
||||
There is no "send to DLQ" operation in AMQP. A dead-letter exchange is a **queue argument**, and
|
||||
messages arrive there as a side effect of three specific events.
|
||||
|
||||
```java
|
||||
QueueBuilder.durable("orders.work")
|
||||
.deadLetterExchange("orders.dlx")
|
||||
.deadLetterRoutingKey("failed")
|
||||
.build();
|
||||
```
|
||||
|
||||
`deadLetterRoutingKey` matters more than it looks. Without it, the message is republished with
|
||||
its **original** routing key, so your DLQ binding has to anticipate every routing key the
|
||||
upstream might have used. Setting it to a constant means one binding catches everything.
|
||||
|
||||
## The three triggers
|
||||
|
||||
| Event | `x-death` reason |
|
||||
|---|---|
|
||||
| `basicNack` / `basicReject` with `requeue=false` | `rejected` |
|
||||
| message TTL expires (`x-message-ttl` or per-message) | `expired` |
|
||||
| queue length or byte limit exceeded (`x-max-length`) | `maxlen` |
|
||||
|
||||
And the one that is **not** a trigger: `requeue=true`. More on that below.
|
||||
|
||||
## What arrives
|
||||
|
||||
Both captured from real runs, in
|
||||
[`docs/output/dead-letter.txt`](output/dead-letter.txt):
|
||||
|
||||
```
|
||||
=== x-death after basicNack(requeue=false) ===
|
||||
reason rejected
|
||||
count 1
|
||||
exchange
|
||||
time Sat Aug 29 09:53:17 IST 2026
|
||||
routing-keys [orders.work]
|
||||
queue orders.work
|
||||
|
||||
=== x-death after x-message-ttl expiry ===
|
||||
reason expired
|
||||
count 1
|
||||
queue orders.ttl
|
||||
```
|
||||
|
||||
Same destination queue, different `reason`. That field is the whole diagnostic: `rejected` means
|
||||
a consumer looked at the work and refused it; `expired` means nobody got to it in time. Those are
|
||||
different incidents with different fixes, and they are indistinguishable without reading the
|
||||
header.
|
||||
|
||||
The third trigger has a detail worth knowing: when `x-max-length` is exceeded RabbitMQ drops from
|
||||
the **head**, so the message that gets dead-lettered is the **oldest** one already queued, not the
|
||||
one that just arrived:
|
||||
|
||||
```
|
||||
=== x-death after x-max-length overflow ===
|
||||
reason maxlen
|
||||
queue orders.bounded
|
||||
body {"orderId":"m-1","detail":"detail for m-1"}
|
||||
```
|
||||
|
||||
Three messages into a queue that holds two, and `m-1` is the one on the DLQ. A bounded queue
|
||||
under sustained overload therefore dead-letters your *backlog* while continuing to accept new
|
||||
work — which is usually what you want for telemetry and exactly wrong for orders.
|
||||
|
||||
`x-death` is a **list**, not a map — one entry per queue the message has been dead-lettered from,
|
||||
and `count` accumulates. That is how you build a retry limit: read
|
||||
`x-death[0].count`, and stop republishing past a threshold.
|
||||
|
||||
## The infinite loop
|
||||
|
||||
```java
|
||||
channel.basicNack(deliveryTag, false, true); // requeue = true
|
||||
```
|
||||
|
||||
From [`docs/output/requeue-loop.txt`](output/requeue-loop.txt), 200 delivery attempts:
|
||||
|
||||
```
|
||||
redelivered 199 times
|
||||
dead-lettered 0
|
||||
still on queue 1
|
||||
```
|
||||
|
||||
The dead-letter exchange is never consulted. `x-death` is never written. There is no counter to
|
||||
exhaust and no backoff. One poison message with an unconditional `requeue=true` handler will
|
||||
saturate a consumer indefinitely, and the queue depth stays at 1 the whole time — so a depth
|
||||
alarm will not fire either. The only visible signal is CPU.
|
||||
|
||||
**`requeue=true` is only correct when the failure is transient and you have a delay.** With no
|
||||
delay, a message that fails because a database is down comes straight back while the database is
|
||||
still down. The `redelivered` flag on the envelope is your one piece of state:
|
||||
|
||||
```java
|
||||
boolean retried = response.getEnvelope().isRedeliver();
|
||||
channel.basicNack(tag, false, !retried); // one requeue, then dead-letter
|
||||
```
|
||||
|
||||
That is a one-attempt retry with no extra infrastructure. For anything more, use a delay queue:
|
||||
a queue with a TTL and a DLX pointing back at the work exchange turns "expire after 30 seconds"
|
||||
into "retry in 30 seconds".
|
||||
|
||||
## Do not point the DLQ's DLX at its own source
|
||||
|
||||
If `orders.dlq` dead-letters to an exchange bound back to `orders.work`, a message that fails
|
||||
consistently cycles between the two forever, gaining an `x-death` entry each time until the
|
||||
header itself is the biggest thing in the message. The DLQ in this module deliberately has no
|
||||
dead-letter exchange at all.
|
||||
|
||||
[Acknowledgement →](05-acknowledgement.md)
|
||||
Reference in New Issue
Block a user