101 lines
3.9 KiB
Java
101 lines
3.9 KiB
Java
package com.ankurm.rabbit;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
import org.springframework.amqp.AmqpIOException;
|
|
import org.springframework.amqp.core.Queue;
|
|
import org.springframework.amqp.core.QueueBuilder;
|
|
import org.springframework.amqp.core.ReturnedMessage;
|
|
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.concurrent.CopyOnWriteArrayList;
|
|
import java.util.List;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
|
|
|
/**
|
|
* Two ways a RabbitMQ topology loses your work quietly.
|
|
*
|
|
* @see <a href="../../../../../docs/03-the-silent-drop.md">docs/03-the-silent-drop.md</a>
|
|
* @see <a href="../../../../../docs/06-changing-your-mind.md">docs/06-changing-your-mind.md</a>
|
|
*/
|
|
@SpringBootTest
|
|
class TopologyTrapsTest {
|
|
|
|
@Autowired
|
|
RabbitTemplate template;
|
|
|
|
@Autowired
|
|
RabbitAdmin admin;
|
|
|
|
@Test
|
|
void anUnroutableMessageIsDiscardedUnlessYouAskForItBack() throws Exception {
|
|
List<ReturnedMessage> returned = new CopyOnWriteArrayList<>();
|
|
this.template.setReturnsCallback(returned::add);
|
|
|
|
// "amend" matches no binding on orders.direct. The broker has nowhere to put it.
|
|
this.template.convertAndSend(Topology.DIRECT, "amend", OrderMessage.of("o-1"));
|
|
for (int i = 0; i < 100 && returned.isEmpty(); i++) {
|
|
Thread.sleep(20);
|
|
}
|
|
|
|
// spring.rabbitmq.template.mandatory=true is what turns a silent discard into a return.
|
|
assertThat(this.template.isMandatoryFor(null)).isTrue();
|
|
assertThat(returned).hasSize(1);
|
|
ReturnedMessage message = returned.get(0);
|
|
System.out.println("=== returned message ===");
|
|
System.out.println(" replyCode " + message.getReplyCode());
|
|
System.out.println(" replyText " + message.getReplyText());
|
|
System.out.println(" exchange " + message.getExchange());
|
|
System.out.println(" routingKey " + message.getRoutingKey());
|
|
|
|
assertThat(message.getReplyCode()).isEqualTo(312);
|
|
assertThat(message.getReplyText()).isEqualTo("NO_ROUTE");
|
|
assertThat(message.getRoutingKey()).isEqualTo("amend");
|
|
// Note what did NOT happen: convertAndSend returned normally. Publishing is fire and
|
|
// forget at the protocol level, so even with mandatory=true the failure arrives
|
|
// asynchronously on another thread. Nothing throws.
|
|
}
|
|
|
|
@Test
|
|
void redeclaringAQueueWithDifferentArgumentsIsAPreconditionFailure() {
|
|
// orders.ttl already exists with x-message-ttl=1500. Same name, different argument.
|
|
Queue conflicting = QueueBuilder.durable(Topology.Q_TTL)
|
|
.ttl(9999)
|
|
.deadLetterExchange(Topology.DLX)
|
|
.deadLetterRoutingKey("failed")
|
|
.build();
|
|
|
|
assertThatExceptionOfType(AmqpIOException.class)
|
|
.isThrownBy(() -> this.admin.declareQueue(conflicting))
|
|
.satisfies((ex) -> {
|
|
String detail = rootMessage(ex);
|
|
System.out.println("=== redeclaring orders.ttl with x-message-ttl=9999 ===");
|
|
System.out.println(" " + detail);
|
|
assertThat(detail).contains("PRECONDITION_FAILED")
|
|
.contains("inequivalent arg 'x-message-ttl'");
|
|
});
|
|
|
|
// Queue arguments are immutable. There is no ALTER QUEUE. Changing a TTL, a max-length
|
|
// or a dead-letter exchange on an existing queue means: declare a new queue, move the
|
|
// consumers, drain the old one, delete it. Plan the rename into the change.
|
|
}
|
|
|
|
private static String rootMessage(Throwable throwable) {
|
|
// The useful text is on the cause. AmqpIOException's own message is just
|
|
// "java.io.IOException", which is why this failure is so often reported as "IOException"
|
|
// with no further detail.
|
|
Throwable current = throwable;
|
|
StringBuilder all = new StringBuilder();
|
|
while (current != null) {
|
|
all.append(current.getMessage()).append(" | ");
|
|
current = current.getCause();
|
|
}
|
|
return all.toString();
|
|
}
|
|
|
|
}
|