Skip to content

Commit 410b584

Browse files
authored
GH-2478: Handle conversion exception in AsyncRabbitTemplate
Fixes: #2478 Previously, conversion errors in `AsyncRabbitTemplate` have led to `AmqpReplyTimeoutException` * Fix `AsyncRabbitTemplate` to catch conversion errors on receiving reply and call `rabbitFuture.completeExceptionally()`, respectively. * Use AssertJ `CompletableFuture` assertions for exception verification
1 parent 7ea51de commit 410b584

File tree

2 files changed

+95
-72
lines changed

2 files changed

+95
-72
lines changed

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder;
5050
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
5151
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
52+
import org.springframework.amqp.support.converter.MessageConversionException;
5253
import org.springframework.amqp.support.converter.MessageConverter;
5354
import org.springframework.amqp.support.converter.SmartMessageConverter;
5455
import org.springframework.amqp.utils.JavaUtils;
@@ -89,6 +90,7 @@
8990
* @author Artem Bilan
9091
* @author FengYang Su
9192
* @author Ngoc Nhan
93+
* @author Ben Efrati
9294
*
9395
* @since 1.6
9496
*/
@@ -604,12 +606,17 @@ public void onMessage(Message message, Channel channel) {
604606
if (future instanceof RabbitConverterFuture) {
605607
MessageConverter messageConverter = this.template.getMessageConverter();
606608
RabbitConverterFuture<Object> rabbitFuture = (RabbitConverterFuture<Object>) future;
607-
Object converted = rabbitFuture.getReturnType() != null
609+
try {
610+
Object converted = rabbitFuture.getReturnType() != null
608611
&& messageConverter instanceof SmartMessageConverter smart
609612
? smart.fromMessage(message,
610613
rabbitFuture.getReturnType())
611614
: messageConverter.fromMessage(message);
612-
rabbitFuture.complete(converted);
615+
rabbitFuture.complete(converted);
616+
}
617+
catch (MessageConversionException e) {
618+
rabbitFuture.completeExceptionally(e);
619+
}
613620
}
614621
else {
615622
((RabbitMessageFuture) future).complete(message);

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java

Lines changed: 86 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
2020
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
21-
import static org.assertj.core.api.Assertions.fail;
2221
import static org.awaitility.Awaitility.await;
2322
import static org.mockito.Mockito.mock;
2423

@@ -57,6 +56,7 @@
5756
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
5857
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
5958
import org.springframework.amqp.rabbit.listener.adapter.ReplyingMessageListener;
59+
import org.springframework.amqp.support.converter.MessageConversionException;
6060
import org.springframework.amqp.support.converter.SimpleMessageConverter;
6161
import org.springframework.amqp.support.postprocessor.GUnzipPostProcessor;
6262
import org.springframework.amqp.support.postprocessor.GZipPostProcessor;
@@ -72,6 +72,7 @@
7272
/**
7373
* @author Gary Russell
7474
* @author Artem Bilan
75+
* @author Ben Efrati
7576
*
7677
* @since 1.6
7778
*/
@@ -100,7 +101,7 @@ static void setup() {
100101
}
101102

102103
@Test
103-
public void testConvert1Arg() throws Exception {
104+
public void testConvert1Arg() {
104105
final AtomicBoolean mppCalled = new AtomicBoolean();
105106
CompletableFuture<String> future = this.asyncTemplate.convertSendAndReceive("foo", m -> {
106107
mppCalled.set(true);
@@ -111,7 +112,7 @@ public void testConvert1Arg() throws Exception {
111112
}
112113

113114
@Test
114-
public void testConvert1ArgDirect() throws Exception {
115+
public void testConvert1ArgDirect() {
115116
this.latch.set(new CountDownLatch(1));
116117
CompletableFuture<String> future1 = this.asyncDirectTemplate.convertSendAndReceive("foo");
117118
CompletableFuture<String> future2 = this.asyncDirectTemplate.convertSendAndReceive("bar");
@@ -139,19 +140,19 @@ public void testConvert1ArgDirect() throws Exception {
139140
}
140141

141142
@Test
142-
public void testConvert2Args() throws Exception {
143+
public void testConvert2Args() {
143144
CompletableFuture<String> future = this.asyncTemplate.convertSendAndReceive(this.requests.getName(), "foo");
144145
checkConverterResult(future, "FOO");
145146
}
146147

147148
@Test
148-
public void testConvert3Args() throws Exception {
149+
public void testConvert3Args() {
149150
CompletableFuture<String> future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo");
150151
checkConverterResult(future, "FOO");
151152
}
152153

153154
@Test
154-
public void testConvert4Args() throws Exception {
155+
public void testConvert4Args() {
155156
CompletableFuture<String> future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo",
156157
message -> {
157158
String body = new String(message.getBody());
@@ -187,7 +188,7 @@ public void testMessage1ArgDirect() throws Exception {
187188
assertThat(TestUtils
188189
.getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount",
189190
AtomicInteger.class).get())
190-
.isEqualTo(0);
191+
.isZero();
191192
}
192193

193194
private void waitForZeroInUseConsumers() {
@@ -214,7 +215,7 @@ public void testMessage3Args() throws Exception {
214215
public void testCancel() {
215216
CompletableFuture<String> future = this.asyncTemplate.convertSendAndReceive("foo");
216217
future.cancel(false);
217-
assertThat(TestUtils.getPropertyValue(asyncTemplate, "pending", Map.class)).hasSize(0);
218+
assertThat(TestUtils.getPropertyValue(asyncTemplate, "pending", Map.class)).isEmpty();
218219
}
219220

220221
@Test
@@ -234,44 +235,46 @@ private Message getFooMessage() {
234235

235236
@Test
236237
@DirtiesContext
237-
public void testReturn() throws Exception {
238+
public void testReturn() {
238239
this.asyncTemplate.setMandatory(true);
239240
CompletableFuture<String> future = this.asyncTemplate.convertSendAndReceive(this.requests.getName() + "x",
240241
"foo");
241-
try {
242-
future.get(10, TimeUnit.SECONDS);
243-
fail("Expected exception");
244-
}
245-
catch (ExecutionException e) {
246-
assertThat(e.getCause()).isInstanceOf(AmqpMessageReturnedException.class);
247-
assertThat(((AmqpMessageReturnedException) e.getCause()).getRoutingKey()).isEqualTo(this.requests.getName() + "x");
248-
}
242+
assertThat(future)
243+
.as("Expected exception")
244+
.failsWithin(Duration.ofSeconds(10))
245+
.withThrowableOfType(ExecutionException.class)
246+
.havingCause()
247+
.isInstanceOf(AmqpMessageReturnedException.class)
248+
.extracting(cause -> ((AmqpMessageReturnedException) cause).getRoutingKey())
249+
.isEqualTo(this.requests.getName() + "x");
249250
}
250251

251252
@Test
252253
@DirtiesContext
253-
public void testReturnDirect() throws Exception {
254+
public void testReturnDirect() {
254255
this.asyncDirectTemplate.setMandatory(true);
255256
CompletableFuture<String> future = this.asyncDirectTemplate.convertSendAndReceive(this.requests.getName() + "x",
256257
"foo");
257-
try {
258-
future.get(10, TimeUnit.SECONDS);
259-
fail("Expected exception");
260-
}
261-
catch (ExecutionException e) {
262-
assertThat(e.getCause()).isInstanceOf(AmqpMessageReturnedException.class);
263-
assertThat(((AmqpMessageReturnedException) e.getCause()).getRoutingKey()).isEqualTo(this.requests.getName() + "x");
264-
}
258+
259+
assertThat(future)
260+
.as("Expected exception")
261+
.failsWithin(Duration.ofSeconds(10))
262+
.withThrowableOfType(ExecutionException.class)
263+
.havingCause()
264+
.isInstanceOf(AmqpMessageReturnedException.class)
265+
.extracting(cause -> ((AmqpMessageReturnedException) cause).getRoutingKey())
266+
.isEqualTo(this.requests.getName() + "x");
265267
}
266268

267269
@Test
268270
@DirtiesContext
269-
public void testConvertWithConfirm() throws Exception {
271+
public void testConvertWithConfirm() {
270272
this.asyncTemplate.setEnableConfirms(true);
271273
RabbitConverterFuture<String> future = this.asyncTemplate.convertSendAndReceive("sleep");
272274
CompletableFuture<Boolean> confirm = future.getConfirm();
273-
assertThat(confirm).isNotNull();
274-
assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue();
275+
assertThat(confirm).isNotNull()
276+
.succeedsWithin(Duration.ofSeconds(10))
277+
.isEqualTo(true);
275278
checkConverterResult(future, "SLEEP");
276279
}
277280

@@ -282,19 +285,21 @@ public void testMessageWithConfirm() throws Exception {
282285
RabbitMessageFuture future = this.asyncTemplate
283286
.sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties()));
284287
CompletableFuture<Boolean> confirm = future.getConfirm();
285-
assertThat(confirm).isNotNull();
286-
assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue();
288+
assertThat(confirm).isNotNull()
289+
.succeedsWithin(Duration.ofSeconds(10))
290+
.isEqualTo(true);
287291
checkMessageResult(future, "SLEEP");
288292
}
289293

290294
@Test
291295
@DirtiesContext
292-
public void testConvertWithConfirmDirect() throws Exception {
296+
public void testConvertWithConfirmDirect() {
293297
this.asyncDirectTemplate.setEnableConfirms(true);
294298
RabbitConverterFuture<String> future = this.asyncDirectTemplate.convertSendAndReceive("sleep");
295299
CompletableFuture<Boolean> confirm = future.getConfirm();
296-
assertThat(confirm).isNotNull();
297-
assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue();
300+
assertThat(confirm).isNotNull()
301+
.succeedsWithin(Duration.ofSeconds(10))
302+
.isEqualTo(true);
298303
checkConverterResult(future, "SLEEP");
299304
}
300305

@@ -305,8 +310,9 @@ public void testMessageWithConfirmDirect() throws Exception {
305310
RabbitMessageFuture future = this.asyncDirectTemplate
306311
.sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties()));
307312
CompletableFuture<Boolean> confirm = future.getConfirm();
308-
assertThat(confirm).isNotNull();
309-
assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue();
313+
assertThat(confirm).isNotNull()
314+
.succeedsWithin(Duration.ofSeconds(10))
315+
.isEqualTo(true);
310316
checkMessageResult(future, "SLEEP");
311317
}
312318

@@ -319,14 +325,12 @@ public void testReceiveTimeout() throws Exception {
319325
TheCallback callback = new TheCallback();
320326
future.whenComplete(callback);
321327
assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1);
322-
try {
323-
future.get(10, TimeUnit.SECONDS);
324-
fail("Expected ExecutionException");
325-
}
326-
catch (ExecutionException e) {
327-
assertThat(e.getCause()).isInstanceOf(AmqpReplyTimeoutException.class);
328-
}
329-
assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(0);
328+
assertThat(future)
329+
.as("Expected ExecutionException")
330+
.failsWithin(Duration.ofSeconds(10))
331+
.withThrowableOfType(ExecutionException.class)
332+
.withCauseInstanceOf(AmqpReplyTimeoutException.class);
333+
assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).isEmpty();
330334
assertThat(callback.latch.await(10, TimeUnit.SECONDS)).isTrue();
331335
assertThat(callback.ex).isInstanceOf(AmqpReplyTimeoutException.class);
332336
}
@@ -340,14 +344,13 @@ public void testReplyAfterReceiveTimeout() throws Exception {
340344
TheCallback callback = new TheCallback();
341345
future.whenComplete(callback);
342346
assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1);
343-
try {
344-
future.get(10, TimeUnit.SECONDS);
345-
fail("Expected ExecutionException");
346-
}
347-
catch (ExecutionException e) {
348-
assertThat(e.getCause()).isInstanceOf(AmqpReplyTimeoutException.class);
349-
}
350-
assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(0);
347+
348+
assertThat(future)
349+
.as("Expected ExecutionException")
350+
.failsWithin(Duration.ofSeconds(10))
351+
.withThrowableOfType(ExecutionException.class)
352+
.withCauseInstanceOf(AmqpReplyTimeoutException.class);
353+
assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).isEmpty();
351354
assertThat(callback.latch.await(10, TimeUnit.SECONDS)).isTrue();
352355
assertThat(callback.ex).isInstanceOf(AmqpReplyTimeoutException.class);
353356

@@ -373,16 +376,17 @@ public void testStopCancelled() throws Exception {
373376
this.asyncTemplate.stop();
374377
// Second stop() to be sure that it is idempotent
375378
this.asyncTemplate.stop();
376-
try {
377-
future.get(10, TimeUnit.SECONDS);
378-
fail("Expected CancellationException");
379-
}
380-
catch (CancellationException e) {
381-
assertThat(future.getNackCause()).isEqualTo("AsyncRabbitTemplate was stopped while waiting for reply");
382-
}
383-
assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(0);
379+
assertThat(future)
380+
.as("Expected CancellationException")
381+
.failsWithin(Duration.ofSeconds(10))
382+
.withThrowableOfType(CancellationException.class)
383+
.satisfies(e -> {
384+
assertThat(future.getNackCause()).isEqualTo("AsyncRabbitTemplate was stopped while waiting for reply");
385+
assertThat(future).isCancelled();
386+
});
387+
388+
assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).isEmpty();
384389
assertThat(callback.latch.await(10, TimeUnit.SECONDS)).isTrue();
385-
assertThat(future.isCancelled()).isTrue();
386390
assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "taskScheduler")).isNull();
387391

388392
/*
@@ -394,6 +398,23 @@ public void testStopCancelled() throws Exception {
394398
assertThat(callback.result).isNull();
395399
}
396400

401+
@Test
402+
@DirtiesContext
403+
public void testConversionException() {
404+
this.asyncTemplate.getRabbitTemplate().setMessageConverter(new SimpleMessageConverter() {
405+
@Override
406+
public Object fromMessage(Message message) throws MessageConversionException {
407+
throw new MessageConversionException("Failed to convert message");
408+
}
409+
});
410+
411+
RabbitConverterFuture<String> replyFuture = this.asyncTemplate.convertSendAndReceive("conversionException");
412+
413+
assertThat(replyFuture).failsWithin(Duration.ofSeconds(10))
414+
.withThrowableThat()
415+
.withCauseInstanceOf(MessageConversionException.class);
416+
}
417+
397418
@Test
398419
void ctorCoverage() {
399420
AsyncRabbitTemplate template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk");
@@ -461,15 +482,10 @@ public void limitedChannelsAreReleasedOnTimeout() {
461482
connectionFactory.destroy();
462483
}
463484

464-
private void checkConverterResult(CompletableFuture<String> future, String expected) throws InterruptedException {
465-
final CountDownLatch cdl = new CountDownLatch(1);
466-
final AtomicReference<String> resultRef = new AtomicReference<>();
467-
future.whenComplete((result, ex) -> {
468-
resultRef.set(result);
469-
cdl.countDown();
470-
});
471-
assertThat(cdl.await(10, TimeUnit.SECONDS)).isTrue();
472-
assertThat(resultRef.get()).isEqualTo(expected);
485+
private void checkConverterResult(CompletableFuture<String> future, String expected) {
486+
assertThat(future)
487+
.succeedsWithin(Duration.ofSeconds(10))
488+
.isEqualTo(expected);
473489
}
474490

475491
private Message checkMessageResult(CompletableFuture<Message> future, String expected) throws InterruptedException {

0 commit comments

Comments
 (0)