Add the rabbitmq module

This commit is contained in:
2026-08-29 09:58:34 +05:30
parent 3a682e496e
commit de9fc5ce4c
27 changed files with 1523 additions and 0 deletions

View 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)