Skip to content

Commit dfcfda1

Browse files
garyrussellartembilan
authored andcommitted
AMQP-825: Add Reply RetryTemplate
JIRA: https://jira.spring.io/browse/AMQP-825 * Fix long line. * Polishing - PR Comments * More polishing **cherry-pick to 2.0.x**
1 parent d4c8377 commit dfcfda1

File tree

8 files changed

+246
-10
lines changed

8 files changed

+246
-10
lines changed

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ project('spring-amqp') {
235235
compile ("org.springframework:spring-messaging:$springVersion", optional)
236236
compile ("org.springframework:spring-oxm:$springVersion", optional)
237237
compile ("org.springframework:spring-context:$springVersion", optional)
238+
compile "org.springframework.retry:spring-retry:$springRetryVersion"
239+
238240
compile ("com.fasterxml.jackson.core:jackson-core:$jackson2Version", optional)
239241
compile ("com.fasterxml.jackson.core:jackson-databind:$jackson2Version", optional)
240242
compile ("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jackson2Version", optional)
@@ -263,8 +265,6 @@ project('spring-rabbit') {
263265
compile "org.springframework:spring-messaging:$springVersion"
264266
compile "org.springframework:spring-tx:$springVersion"
265267

266-
compile "org.springframework.retry:spring-retry:$springRetryVersion"
267-
268268
compile ("ch.qos.logback:logback-classic:$logbackVersion", optional)
269269

270270
compile ("org.apache.logging.log4j:log4j-core:$log4jVersion", optional)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.amqp.support;
18+
19+
import org.springframework.amqp.core.Address;
20+
import org.springframework.amqp.core.Message;
21+
import org.springframework.retry.RetryContext;
22+
23+
/**
24+
* Type safe accessor for retried message sending.
25+
*
26+
* @author Gary Russell
27+
* @since 2.0.6
28+
*
29+
*/
30+
public final class SendRetryContextAccessor {
31+
32+
/**
33+
* Key for the message we tried to send.
34+
*/
35+
public static final String MESSAGE = "message";
36+
37+
/**
38+
* Key for the Address we tried to send to.
39+
*/
40+
public static final String ADDRESS = "address";
41+
42+
private SendRetryContextAccessor() {
43+
super();
44+
}
45+
46+
/**
47+
* Retrieve the {@link Message} from the context.
48+
* @param context the context.
49+
* @return the message.
50+
* @see #MESSAGE
51+
*/
52+
public static Message getMessage(RetryContext context) {
53+
return (Message) context.getAttribute(MESSAGE);
54+
}
55+
56+
/**
57+
* Retrieve the {@link Address} from the context.
58+
* @param context the context.
59+
* @return the address.
60+
* @see #ADDRESS
61+
*/
62+
public static Address getAddress(RetryContext context) {
63+
return (Address) context.getAttribute(ADDRESS);
64+
}
65+
66+
}

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
import org.springframework.context.ApplicationContextAware;
4040
import org.springframework.context.ApplicationEventPublisher;
4141
import org.springframework.context.ApplicationEventPublisherAware;
42+
import org.springframework.retry.RecoveryCallback;
43+
import org.springframework.retry.support.RetryTemplate;
4244
import org.springframework.transaction.PlatformTransactionManager;
4345
import org.springframework.util.ErrorHandler;
4446
import org.springframework.util.backoff.BackOff;
@@ -106,6 +108,10 @@ public abstract class AbstractRabbitListenerContainerFactory<C extends AbstractM
106108

107109
private MessagePostProcessor[] beforeSendReplyPostProcessors;
108110

111+
private RetryTemplate retryTemplate;
112+
113+
private RecoveryCallback<?> recoveryCallback;
114+
109115
protected final AtomicInteger counter = new AtomicInteger();
110116

111117
/**
@@ -287,14 +293,41 @@ public void setAfterReceivePostProcessors(MessagePostProcessor... afterReceivePo
287293
}
288294

289295
/**
290-
* Set post processors that will be applied before sending replies.
296+
* Set post processors that will be applied before sending replies; added to each
297+
* message listener adapter.
291298
* @param beforeSendReplyPostProcessors the post processors.
292299
* @since 2.0.3
300+
* @see AbstractAdaptableMessageListener#setBeforeSendReplyPostProcessors(MessagePostProcessor...)
293301
*/
294302
public void setBeforeSendReplyPostProcessors(MessagePostProcessor... beforeSendReplyPostProcessors) {
295303
this.beforeSendReplyPostProcessors = beforeSendReplyPostProcessors;
296304
}
297305

306+
/**
307+
* Set a {@link RetryTemplate} to use when sending replies; added to each message
308+
* listener adapter.
309+
* @param retryTemplate the template.
310+
* @since 2.0.6
311+
* @see #setReplyRecoveryCallback(RecoveryCallback)
312+
* @see AbstractAdaptableMessageListener#setRetryTemplate(RetryTemplate)
313+
*/
314+
public void setRetryTemplate(RetryTemplate retryTemplate) {
315+
this.retryTemplate = retryTemplate;
316+
}
317+
318+
/**
319+
* Set a {@link RecoveryCallback} to invoke when retries are exhausted. Added to each
320+
* message listener adapter. Only used if a {@link #setRetryTemplate(RetryTemplate)
321+
* retryTemplate} is provided.
322+
* @param recoveryCallback the recovery callback.
323+
* @since 2.0.6
324+
* @see #setRetryTemplate(RetryTemplate)
325+
* @see AbstractAdaptableMessageListener#setRecoveryCallback(RecoveryCallback)
326+
*/
327+
public void setReplyRecoveryCallback(RecoveryCallback<?> recoveryCallback) {
328+
this.recoveryCallback = recoveryCallback;
329+
}
330+
298331
@Override
299332
public C createListenerContainer(RabbitListenerEndpoint endpoint) {
300333
C instance = createContainerInstance();
@@ -370,10 +403,18 @@ public C createListenerContainer(RabbitListenerEndpoint endpoint) {
370403

371404
endpoint.setupListenerContainer(instance);
372405
}
373-
if (this.beforeSendReplyPostProcessors != null
374-
&& instance.getMessageListener() instanceof AbstractAdaptableMessageListener) {
375-
((AbstractAdaptableMessageListener) instance.getMessageListener())
376-
.setBeforeSendReplyPostProcessors(this.beforeSendReplyPostProcessors);
406+
if (instance.getMessageListener() instanceof AbstractAdaptableMessageListener) {
407+
AbstractAdaptableMessageListener messageListener = (AbstractAdaptableMessageListener) instance
408+
.getMessageListener();
409+
if (this.beforeSendReplyPostProcessors != null) {
410+
messageListener.setBeforeSendReplyPostProcessors(this.beforeSendReplyPostProcessors);
411+
}
412+
if (this.retryTemplate != null) {
413+
messageListener.setRetryTemplate(this.retryTemplate);
414+
if (this.recoveryCallback != null) {
415+
messageListener.setRecoveryCallback(this.recoveryCallback);
416+
}
417+
}
377418
}
378419
initializeContainer(instance, endpoint);
379420

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.amqp.rabbit.listener.adapter;
1818

19+
import java.io.IOException;
1920
import java.lang.reflect.Type;
2021
import java.util.Arrays;
2122

@@ -32,6 +33,7 @@
3233
import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter;
3334
import org.springframework.amqp.rabbit.support.MessagePropertiesConverter;
3435
import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator;
36+
import org.springframework.amqp.support.SendRetryContextAccessor;
3537
import org.springframework.amqp.support.converter.MessageConversionException;
3638
import org.springframework.amqp.support.converter.MessageConverter;
3739
import org.springframework.amqp.support.converter.SimpleMessageConverter;
@@ -43,6 +45,8 @@
4345
import org.springframework.expression.spel.standard.SpelExpressionParser;
4446
import org.springframework.expression.spel.support.StandardEvaluationContext;
4547
import org.springframework.expression.spel.support.StandardTypeConverter;
48+
import org.springframework.retry.RecoveryCallback;
49+
import org.springframework.retry.support.RetryTemplate;
4650
import org.springframework.util.Assert;
4751

4852
import com.rabbitmq.client.Channel;
@@ -92,6 +96,11 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe
9296

9397
private MessagePostProcessor[] beforeSendReplyPostProcessors;
9498

99+
private RetryTemplate retryTemplate;
100+
101+
private RecoveryCallback<?> recoveryCallback;
102+
103+
95104
/**
96105
* Set the routing key to use when sending response messages.
97106
* This will be applied in case of a request message that
@@ -196,6 +205,26 @@ public void setBeforeSendReplyPostProcessors(MessagePostProcessor... beforeSendR
196205
beforeSendReplyPostProcessors.length);
197206
}
198207

208+
/**
209+
* Set a {@link RetryTemplate} to use when sending replies.
210+
* @param retryTemplate the template.
211+
* @since 2.0.6
212+
* @see #setRecoveryCallback(RecoveryCallback)
213+
*/
214+
public void setRetryTemplate(RetryTemplate retryTemplate) {
215+
this.retryTemplate = retryTemplate;
216+
}
217+
218+
/**
219+
* Set a {@link RecoveryCallback} to invoke when retries are exhausted.
220+
* @param recoveryCallback the recovery callback.
221+
* @since 2.0.6
222+
* @see #setRetryTemplate(RetryTemplate)
223+
*/
224+
public void setRecoveryCallback(RecoveryCallback<?> recoveryCallback) {
225+
this.recoveryCallback = recoveryCallback;
226+
}
227+
199228
/**
200229
* Set a bean resolver for runtime SpEL expressions. Also configures the evaluation
201230
* context with a standard type converter and map accessor.
@@ -414,15 +443,38 @@ protected void sendResponse(Channel channel, Address replyTo, Message messageIn)
414443
try {
415444
this.logger.debug("Publishing response to exchange = [" + replyTo.getExchangeName() + "], routingKey = ["
416445
+ replyTo.getRoutingKey() + "]");
417-
channel.basicPublish(replyTo.getExchangeName(), replyTo.getRoutingKey(), this.mandatoryPublish,
418-
this.messagePropertiesConverter.fromMessageProperties(message.getMessageProperties(), this.encoding),
419-
message.getBody());
446+
if (this.retryTemplate == null) {
447+
doPublish(channel, replyTo, message);
448+
}
449+
else {
450+
final Message messageToSend = message;
451+
this.retryTemplate.execute(ctx -> {
452+
doPublish(channel, replyTo, messageToSend);
453+
return null;
454+
}, ctx -> {
455+
if (this.recoveryCallback != null) {
456+
ctx.setAttribute(SendRetryContextAccessor.MESSAGE, messageToSend);
457+
ctx.setAttribute(SendRetryContextAccessor.ADDRESS, replyTo);
458+
this.recoveryCallback.recover(ctx);
459+
return null;
460+
}
461+
else {
462+
throw RabbitExceptionTranslator.convertRabbitAccessException(ctx.getLastThrowable());
463+
}
464+
});
465+
}
420466
}
421467
catch (Exception ex) {
422468
throw RabbitExceptionTranslator.convertRabbitAccessException(ex);
423469
}
424470
}
425471

472+
protected void doPublish(Channel channel, Address replyTo, Message message) throws IOException {
473+
channel.basicPublish(replyTo.getExchangeName(), replyTo.getRoutingKey(), this.mandatoryPublish,
474+
this.messagePropertiesConverter.fromMessageProperties(message.getMessageProperties(), this.encoding),
475+
message.getBody());
476+
}
477+
426478
/**
427479
* Post-process the given message before sending the response.
428480
* <p>

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
128128
import org.springframework.messaging.support.GenericMessage;
129129
import org.springframework.messaging.support.MessageBuilder;
130+
import org.springframework.retry.support.RetryTemplate;
130131
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
131132
import org.springframework.test.annotation.DirtiesContext;
132133
import org.springframework.test.context.ContextConfiguration;
@@ -249,6 +250,9 @@ public void autoSimpleDeclareAnonymousQueue() {
249250
.getListenerContainer("anonymousQueue575");
250251
assertThat(container.getQueueNames(), arrayWithSize(1));
251252
assertEquals("viaAnonymous:foo", rabbitTemplate.convertSendAndReceive(container.getQueueNames()[0], "foo"));
253+
Object messageListener = container.getMessageListener();
254+
assertThat(TestUtils.getPropertyValue(messageListener, "retryTemplate"), notNullValue());
255+
assertThat(TestUtils.getPropertyValue(messageListener, "recoveryCallback"), notNullValue());
252256
}
253257

254258
@Test
@@ -1341,6 +1345,8 @@ public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
13411345
m.getMessageProperties().getHeaders().put("replyMPPApplied", true);
13421346
return m;
13431347
});
1348+
factory.setRetryTemplate(new RetryTemplate());
1349+
factory.setReplyRecoveryCallback(c -> null);
13441350
return factory;
13451351
}
13461352

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,36 @@
1616

1717
package org.springframework.amqp.rabbit.listener.adapter;
1818

19+
import static org.hamcrest.Matchers.equalTo;
20+
import static org.hamcrest.Matchers.notNullValue;
21+
import static org.hamcrest.Matchers.sameInstance;
1922
import static org.junit.Assert.assertEquals;
23+
import static org.junit.Assert.assertThat;
2024
import static org.junit.Assert.assertTrue;
25+
import static org.mockito.ArgumentMatchers.any;
26+
import static org.mockito.ArgumentMatchers.eq;
27+
import static org.mockito.BDDMockito.willThrow;
28+
import static org.mockito.Mockito.mock;
2129

2230
import java.util.HashMap;
2331
import java.util.Map;
2432
import java.util.concurrent.atomic.AtomicBoolean;
33+
import java.util.concurrent.atomic.AtomicReference;
2534

2635
import org.junit.Before;
2736
import org.junit.Test;
2837

38+
import org.springframework.amqp.core.Address;
2939
import org.springframework.amqp.core.Message;
3040
import org.springframework.amqp.core.MessageProperties;
41+
import org.springframework.amqp.support.SendRetryContextAccessor;
3142
import org.springframework.amqp.support.converter.SimpleMessageConverter;
3243
import org.springframework.aop.framework.ProxyFactory;
44+
import org.springframework.retry.RetryPolicy;
45+
import org.springframework.retry.policy.SimpleRetryPolicy;
46+
import org.springframework.retry.support.RetryTemplate;
47+
48+
import com.rabbitmq.client.Channel;
3349

3450
/**
3551
* @author Dave Syer
@@ -131,6 +147,39 @@ public void testJdkProxyListener() throws Exception {
131147
assertEquals("handle", this.simpleService.called);
132148
}
133149

150+
@Test
151+
public void testReplyRetry() throws Exception {
152+
this.adapter.setDefaultListenerMethod("handle");
153+
this.adapter.setDelegate(this.simpleService);
154+
RetryPolicy retryPolicy = new SimpleRetryPolicy(2);
155+
RetryTemplate retryTemplate = new RetryTemplate();
156+
retryTemplate.setRetryPolicy(retryPolicy);
157+
this.adapter.setRetryTemplate(retryTemplate);
158+
AtomicReference<Message> replyMessage = new AtomicReference<>();
159+
AtomicReference<Address> replyAddress = new AtomicReference<>();
160+
AtomicReference<Throwable> throwable = new AtomicReference<>();
161+
this.adapter.setRecoveryCallback(ctx -> {
162+
replyMessage.set(SendRetryContextAccessor.getMessage(ctx));
163+
replyAddress.set(SendRetryContextAccessor.getAddress(ctx));
164+
throwable.set(ctx.getLastThrowable());
165+
return null;
166+
});
167+
this.messageProperties.setReplyTo("foo/bar");
168+
Channel channel = mock(Channel.class);
169+
RuntimeException ex = new RuntimeException();
170+
willThrow(ex).given(channel)
171+
.basicPublish(eq("foo"), eq("bar"), eq(Boolean.FALSE), any(), any());
172+
Message message = new Message("foo".getBytes(), this.messageProperties);
173+
this.adapter.onMessage(message, channel);
174+
assertThat(this.simpleService.called, equalTo("handle"));
175+
assertThat(replyMessage.get(), notNullValue());
176+
assertThat(new String(replyMessage.get().getBody()), equalTo("processedfoo"));
177+
assertThat(replyAddress.get(), notNullValue());
178+
assertThat(replyAddress.get().getExchangeName(), equalTo("foo"));
179+
assertThat(replyAddress.get().getRoutingKey(), equalTo("bar"));
180+
assertThat(throwable.get(), sameInstance(ex));
181+
}
182+
134183
public interface Service {
135184

136185
String handle(String input);

0 commit comments

Comments
 (0)