1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
2919282a5c Add the kafka-error-handling module 2026-08-29 10:14:23 +05:30
8b5587cf88 Add the rabbitmq module 2026-08-29 09:58:34 +05:30
6 changed files with 7 additions and 105 deletions

View File

@@ -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/)
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
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 |
| [`unroutable.txt`](docs/output/unroutable.txt) | `312 NO_ROUTE` |
| [`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

View File

@@ -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
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.

View File

@@ -2,21 +2,13 @@
reason rejected
count 1
exchange
time Sat Aug 29 10:30:04 IST 2026
time Sat Aug 29 09:55:45 IST 2026
routing-keys [orders.work]
queue orders.work
=== x-death after x-message-ttl expiry ===
reason expired
count 1
exchange
time Sat Aug 29 10:30:06 IST 2026
time Sat Aug 29 09:55:47 IST 2026
routing-keys [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"}

View File

@@ -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: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.714 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.476 s -- in com.ankurm.rabbit.RoutingTest
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.797 s -- in com.ankurm.rabbit.DeadLetterTest
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.511 s -- in com.ankurm.rabbit.RoutingTest

View File

@@ -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 '/=== 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-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 '/=== 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

View File

@@ -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");
}
}