Compare commits
2 Commits
f4b0112e2c
...
2919282a5c
| Author | SHA1 | Date | |
|---|---|---|---|
| 2919282a5c | |||
| 8b5587cf88 |
@@ -4,7 +4,7 @@ Companion project for
|
|||||||
[**Spring Boot and RabbitMQ: Exchanges, Queues, Bindings and a Working Dead-Letter Queue**](https://ankurm.com/spring-boot-rabbitmq-exchanges-dead-letter-queue/)
|
[**Spring Boot and RabbitMQ: Exchanges, Queues, Bindings and a Working Dead-Letter Queue**](https://ankurm.com/spring-boot-rabbitmq-exchanges-dead-letter-queue/)
|
||||||
on ankurm.com.
|
on ankurm.com.
|
||||||
|
|
||||||
Ten tests against a **real RabbitMQ broker**. All four exchange types, manual acknowledgement,
|
Nine tests against a **real RabbitMQ broker**. All four exchange types, manual acknowledgement,
|
||||||
a dead-letter path exercised through `basicNack` and through TTL expiry, and the three ways a
|
a dead-letter path exercised through `basicNack` and through TTL expiry, and the three ways a
|
||||||
topology loses work quietly.
|
topology loses work quietly.
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export ERL_ROOT=/path/to/erlang RABBITMQ_HOME=/path/to/rabbitmq_server-3.10.25
|
|||||||
| [`requeue-loop.txt`](docs/output/requeue-loop.txt) | 199 redeliveries, 0 dead-lettered |
|
| [`requeue-loop.txt`](docs/output/requeue-loop.txt) | 199 redeliveries, 0 dead-lettered |
|
||||||
| [`unroutable.txt`](docs/output/unroutable.txt) | `312 NO_ROUTE` |
|
| [`unroutable.txt`](docs/output/unroutable.txt) | `312 NO_ROUTE` |
|
||||||
| [`precondition-failed.txt`](docs/output/precondition-failed.txt) | `406` on an inequivalent argument |
|
| [`precondition-failed.txt`](docs/output/precondition-failed.txt) | `406` on an inequivalent argument |
|
||||||
| [`tests.txt`](docs/output/tests.txt) | 10 tests |
|
| [`tests.txt`](docs/output/tests.txt) | 9 tests |
|
||||||
|
|
||||||
## Five things this module exists to prove
|
## Five things this module exists to prove
|
||||||
|
|
||||||
|
|||||||
@@ -51,21 +51,6 @@ a consumer looked at the work and refused it; `expired` means nobody got to it i
|
|||||||
different incidents with different fixes, and they are indistinguishable without reading the
|
different incidents with different fixes, and they are indistinguishable without reading the
|
||||||
header.
|
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,
|
`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
|
and `count` accumulates. That is how you build a retry limit: read
|
||||||
`x-death[0].count`, and stop republishing past a threshold.
|
`x-death[0].count`, and stop republishing past a threshold.
|
||||||
|
|||||||
@@ -2,21 +2,13 @@
|
|||||||
reason rejected
|
reason rejected
|
||||||
count 1
|
count 1
|
||||||
exchange
|
exchange
|
||||||
time Sat Aug 29 10:30:04 IST 2026
|
time Sat Aug 29 09:55:45 IST 2026
|
||||||
routing-keys [orders.work]
|
routing-keys [orders.work]
|
||||||
queue orders.work
|
queue orders.work
|
||||||
=== x-death after x-message-ttl expiry ===
|
=== x-death after x-message-ttl expiry ===
|
||||||
reason expired
|
reason expired
|
||||||
count 1
|
count 1
|
||||||
exchange
|
exchange
|
||||||
time Sat Aug 29 10:30:06 IST 2026
|
time Sat Aug 29 09:55:47 IST 2026
|
||||||
routing-keys [orders.ttl]
|
routing-keys [orders.ttl]
|
||||||
queue orders.ttl
|
queue orders.ttl
|
||||||
=== x-death after x-max-length overflow ===
|
|
||||||
reason maxlen
|
|
||||||
count 1
|
|
||||||
exchange orders.direct
|
|
||||||
time Sat Aug 29 10:30:06 IST 2026
|
|
||||||
routing-keys [bounded]
|
|
||||||
queue orders.bounded
|
|
||||||
body {"orderId":"m-1","detail":"detail for m-1"}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.132 s -- in com.ankurm.rabbit.TopologyTrapsTest
|
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.178 s -- in com.ankurm.rabbit.TopologyTrapsTest
|
||||||
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.714 s -- in com.ankurm.rabbit.DeadLetterTest
|
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.797 s -- in com.ankurm.rabbit.DeadLetterTest
|
||||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.061 s -- in com.ankurm.rabbit.MaxLengthTest
|
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.511 s -- in com.ankurm.rabbit.RoutingTest
|
||||||
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.476 s -- in com.ankurm.rabbit.RoutingTest
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ mvn -B test 2>&1 | tee /tmp/rabbit-test.log > /dev/null
|
|||||||
sed -n '/=== topic exchange ===/,/^order\.high/p' /tmp/rabbit-test.log > docs/output/topic-wildcards.txt
|
sed -n '/=== topic exchange ===/,/^order\.high/p' /tmp/rabbit-test.log > docs/output/topic-wildcards.txt
|
||||||
sed -n '/=== x-death after basicNack/,/^ queue/p' /tmp/rabbit-test.log > docs/output/dead-letter.txt
|
sed -n '/=== x-death after basicNack/,/^ queue/p' /tmp/rabbit-test.log > docs/output/dead-letter.txt
|
||||||
sed -n '/=== x-death after x-message-ttl/,/^ queue/p' /tmp/rabbit-test.log >> docs/output/dead-letter.txt
|
sed -n '/=== x-death after x-message-ttl/,/^ queue/p' /tmp/rabbit-test.log >> docs/output/dead-letter.txt
|
||||||
sed -n '/=== x-death after x-max-length/,/^ body/p' /tmp/rabbit-test.log >> docs/output/dead-letter.txt
|
|
||||||
sed -n '/=== basicNack(requeue=true)/,/still on queue/p' /tmp/rabbit-test.log > docs/output/requeue-loop.txt
|
sed -n '/=== basicNack(requeue=true)/,/still on queue/p' /tmp/rabbit-test.log > docs/output/requeue-loop.txt
|
||||||
sed -n '/=== returned message ===/,/routingKey/p' /tmp/rabbit-test.log > docs/output/unroutable.txt
|
sed -n '/=== returned message ===/,/routingKey/p' /tmp/rabbit-test.log > docs/output/unroutable.txt
|
||||||
sed -n '/=== redeclaring orders.ttl/,+1p' /tmp/rabbit-test.log > docs/output/precondition-failed.txt
|
sed -n '/=== redeclaring orders.ttl/,+1p' /tmp/rabbit-test.log > docs/output/precondition-failed.txt
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
package com.ankurm.rabbit;
|
|
||||||
|
|
||||||
import com.rabbitmq.client.GetResponse;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.amqp.core.Binding;
|
|
||||||
import org.springframework.amqp.core.BindingBuilder;
|
|
||||||
import org.springframework.amqp.core.DirectExchange;
|
|
||||||
import org.springframework.amqp.core.Queue;
|
|
||||||
import org.springframework.amqp.core.QueueBuilder;
|
|
||||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
|
||||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The third dead-letter trigger: a queue that is full.
|
|
||||||
*
|
|
||||||
* <p>Included because the article states all three triggers in one table, and this was the only
|
|
||||||
* row not produced by a run. It is now.
|
|
||||||
*
|
|
||||||
* @see <a href="../../../../../docs/04-dead-lettering.md">docs/04-dead-lettering.md</a>
|
|
||||||
*/
|
|
||||||
@SpringBootTest
|
|
||||||
class MaxLengthTest {
|
|
||||||
|
|
||||||
static final String Q_BOUNDED = "orders.bounded";
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
RabbitTemplate template;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
RabbitAdmin admin;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
void exceedingMaxLengthDeadLettersTheOldestWithReasonMaxlen() throws Exception {
|
|
||||||
this.admin.deleteQueue(Q_BOUNDED);
|
|
||||||
Queue bounded = QueueBuilder.durable(Q_BOUNDED)
|
|
||||||
.maxLength(2)
|
|
||||||
.deadLetterExchange(Topology.DLX)
|
|
||||||
.deadLetterRoutingKey("failed")
|
|
||||||
.build();
|
|
||||||
this.admin.declareQueue(bounded);
|
|
||||||
Binding binding = BindingBuilder.bind(bounded)
|
|
||||||
.to(new DirectExchange(Topology.DIRECT)).with("bounded");
|
|
||||||
this.admin.declareBinding(binding);
|
|
||||||
this.admin.purgeQueue(Topology.Q_DLQ, false);
|
|
||||||
|
|
||||||
// Three messages into a queue that holds two. RabbitMQ drops from the HEAD, so the
|
|
||||||
// FIRST message is the one dead-lettered - the oldest, not the newest.
|
|
||||||
for (String id : List.of("m-1", "m-2", "m-3")) {
|
|
||||||
this.template.convertAndSend(Topology.DIRECT, "bounded", OrderMessage.of(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
var message = this.template.receive(Topology.Q_DLQ, 10_000);
|
|
||||||
assertThat(message).isNotNull();
|
|
||||||
List<Map<String, Object>> deaths =
|
|
||||||
(List<Map<String, Object>>) message.getMessageProperties().getHeader("x-death");
|
|
||||||
System.out.println("=== x-death after x-max-length overflow ===");
|
|
||||||
deaths.get(0).forEach((k, v) -> System.out.printf(" %-16s %s%n", k, v));
|
|
||||||
System.out.println(" body " + new String(message.getBody()));
|
|
||||||
|
|
||||||
assertThat(deaths.get(0)).containsEntry("reason", "maxlen");
|
|
||||||
assertThat(deaths.get(0)).containsEntry("queue", Q_BOUNDED);
|
|
||||||
assertThat(new String(message.getBody())).contains("m-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user