Skip to content

Commit 346d3ae

Browse files
snicollartembilan
authored andcommitted
Replace Spring Retry usage to core retry
This commit replaces Spring Retry by the core retry support introduced in Spring Framework 7. This is a breaking change mostly in configuration that is detailed below. Some of the features used from Spring Retry have no equivalent in Spring Framework, and a tailored solution for Spring AMQP has been implemented in this commit. This leads to the change in the following hook points: * `AbstractAdaptableMessageListener` can be configured by the newly introduced `MessageRecoveryCallback` instead of Spring Retry's `MethodInvocationRecoverer`. The new callback directly provides the Message, replyTo Address, and Exception. * The `RecoveryCallback` in `RabbitTemplate` is replaced by an interface with the same name. * `AbstractRetryOperationsInterceptorFactoryBean` should be configured with a `RetryPolicy`, rather than a `RetryTemplate`. Spring AMQP uses stateless and stateful retry interceptors from Spring Retry. These are replaced by: * `StatelessRetryOperationsInterceptor` that invokes recovery if provided. * `StatefulRetryOperationsInterceptor` provides a similar interceptor as the Spring Retry equivalent, except that it is configured with Spring AMQP-specify callbacks. These interceptors are configured with a `RetryPolicy`, rather than a `RetryTemplate`. The new `RetryTemplate` in Spring Framework is operating on a `RetryPolicy` can can't be further configured. This has an impact on `RetryInterceptorBuilder` where a custom `BackOff` can no longer be specified. Instead, a consumer of the `RetryPolicy.Builder` is provided that offers configuration for it and more. Using dedicated callbacks also shows inconsistencies with the Null-safety support. The most annoying example is MessageKeyGenerator that should be nullable despite its contract stating that it should not. Spring AMQP has actually a code path to deal with stateful retries when the key is null. A major difference in default is that Spring Retry has a default policy of 3 attempts: the initial invocation and 2 retries. Spring Framework has a default RetryPolicy of 3 attempts, that does not include the initial invocation. With 4 attempts in total, there is an off-by-one change in `AmqpAppender` and tests to configure a retry policy with 2 attempts. With Spring Retry being completely removed, this commit also removes the dependency and any further references to it. Signed-off-by: Stéphane Nicoll <[email protected]>
1 parent c43cfe8 commit 346d3ae

File tree

32 files changed

+1022
-568
lines changed

32 files changed

+1022
-568
lines changed

build.gradle

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ ext {
5555
rabbitmqVersion = '5.26.0'
5656
reactorVersion = '2025.0.0-M6'
5757
springDataVersion = '2025.1.0-SNAPSHOT'
58-
springRetryVersion = '2.0.12'
5958
springVersion = '7.0.0-SNAPSHOT'
6059
testcontainersVersion = '1.21.3'
6160

@@ -365,7 +364,6 @@ project('spring-rabbit') {
365364
api 'org.springframework:spring-messaging'
366365
api 'org.springframework:spring-tx'
367366
api 'io.micrometer:micrometer-observation'
368-
api "org.springframework.retry:spring-retry:$springRetryVersion"
369367

370368
optionalApi 'org.springframework:spring-aop'
371369
optionalApi 'org.springframework:spring-webflux'
@@ -708,10 +706,9 @@ def generateAttributes() {
708706
'project-version': project.version,
709707
'spring-integration-docs': "$springDocs/spring-integration/reference".toString(),
710708
'spring-framework-docs': "$springDocs/spring-framework/reference/${generateVersionWithoutPatch(springVersion)}".toString(),
711-
'spring-retry-java-docs': "$springDocs/spring-retry/docs/$springRetryVersion/apidocs".toString(),
709+
'spring-framework-javadoc': "$springDocs/spring-framework/docs/$springVersion/javadoc-api".toString(),
712710
'javadoc-location-org-springframework-transaction': "$springDocs/spring-framework/docs/$springVersion/javadoc-api".toString(),
713711
'javadoc-location-org-springframework-amqp': "$springDocs/spring-amqp/docs/$project.version/api".toString(),
714-
'javadoc-location-org-springframework-retry': "{spring-retry-java-docs}",
715712
'micrometer-docs': "$micrometerDocsPrefix/micrometer/reference/${generateVersionWithoutPatch(micrometerVersion)}".toString(),
716713
'micrometer-tracing-docs': "$micrometerDocsPrefix/tracing/reference/${generateVersionWithoutPatch(micrometerTracingVersion)}".toString()
717714
]

spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,49 +18,49 @@
1818

1919
import com.rabbitmq.stream.Message;
2020
import com.rabbitmq.stream.MessageHandler.Context;
21+
import org.jspecify.annotations.Nullable;
2122

2223
import org.springframework.amqp.rabbit.config.StatelessRetryOperationsInterceptorFactoryBean;
2324
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
25+
import org.springframework.core.retry.RetryOperations;
2426
import org.springframework.rabbit.stream.listener.StreamListenerContainer;
25-
import org.springframework.retry.RetryOperations;
26-
import org.springframework.retry.interceptor.MethodInvocationRecoverer;
27-
import org.springframework.retry.support.RetryTemplate;
27+
import org.springframework.util.Assert;
2828

2929
/**
3030
* Convenient factory bean for creating a stateless retry interceptor for use in a
3131
* {@link StreamListenerContainer} when consuming native stream messages, giving you a
3232
* large amount of control over the behavior of a container when a listener fails. To
3333
* control the number of retry attempt or the backoff in between attempts, supply a
34-
* customized {@link RetryTemplate}. Stateless retry is appropriate if your listener can
34+
* customized {@link RetryOperations}. Stateless retry is appropriate if your listener can
3535
* be called repeatedly between failures with no side effects. The semantics of stateless
3636
* retry mean that a listener exception is not propagated to the container until the retry
3737
* attempts are exhausted. When the retry attempts are exhausted it can be processed using
3838
* a {@link StreamMessageRecoverer} if one is provided.
3939
*
4040
* @author Gary Russell
41-
*
42-
* @see RetryOperations#execute(org.springframework.retry.RetryCallback,org.springframework.retry.RecoveryCallback)
4341
*/
4442
public class StreamRetryOperationsInterceptorFactoryBean extends StatelessRetryOperationsInterceptorFactoryBean {
4543

4644
@Override
47-
protected MethodInvocationRecoverer<?> createRecoverer() {
48-
return (args, cause) -> {
49-
StreamMessageRecoverer messageRecoverer = (StreamMessageRecoverer) getMessageRecoverer();
50-
Object arg = args[0];
51-
if (arg instanceof org.springframework.amqp.core.Message) {
52-
return super.recover(args, cause);
45+
protected @Nullable Object recover(@Nullable Object[] args, Throwable cause) {
46+
StreamMessageRecoverer messageRecoverer = (StreamMessageRecoverer) getMessageRecoverer();
47+
Object arg = args[0];
48+
if (arg instanceof org.springframework.amqp.core.Message) {
49+
return super.recover(args, cause);
50+
}
51+
else {
52+
if (messageRecoverer == null) {
53+
this.logger.warn("Message(s) dropped on recovery: " + arg, cause);
5354
}
5455
else {
55-
if (messageRecoverer == null) {
56-
this.logger.warn("Message(s) dropped on recovery: " + arg, cause);
57-
}
58-
else {
59-
messageRecoverer.recover((Message) arg, (Context) args[1], cause);
60-
}
61-
return null;
56+
Message message = (Message) arg;
57+
Context context = (Context) args[1];
58+
Assert.notNull(message, "Message must not be null");
59+
Assert.notNull(context, "Context must not be null");
60+
messageRecoverer.recover(message, context, cause);
6261
}
63-
};
62+
return null;
63+
}
6464
}
6565

6666
/**

spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
4646
import org.springframework.amqp.rabbit.annotation.RabbitListener;
4747
import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder;
48+
import org.springframework.amqp.rabbit.config.StatelessRetryOperationsInterceptor;
4849
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
4950
import org.springframework.amqp.rabbit.core.RabbitAdmin;
5051
import org.springframework.amqp.rabbit.core.RabbitTemplate;
@@ -62,7 +63,6 @@
6263
import org.springframework.rabbit.stream.retry.StreamRetryOperationsInterceptorFactoryBean;
6364
import org.springframework.rabbit.stream.support.StreamAdmin;
6465
import org.springframework.rabbit.stream.support.StreamMessageProperties;
65-
import org.springframework.retry.interceptor.RetryOperationsInterceptor;
6666
import org.springframework.test.annotation.DirtiesContext;
6767
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
6868
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
@@ -306,7 +306,7 @@ public StreamRetryOperationsInterceptorFactoryBean sfb() {
306306
@Bean
307307
@DependsOn("sfb")
308308
RabbitListenerContainerFactory<StreamListenerContainer> nativeFactory(Environment env,
309-
RetryOperationsInterceptor retry) {
309+
StatelessRetryOperationsInterceptor retry) {
310310

311311
StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env);
312312
factory.setNativeListener(true);
@@ -323,8 +323,7 @@ RabbitListenerContainerFactory<StreamListenerContainer> nativeFactory(Environmen
323323
}
324324

325325
@Bean
326-
RabbitListenerContainerFactory<StreamListenerContainer> nativeObsFactory(Environment env,
327-
RetryOperationsInterceptor retry) {
326+
RabbitListenerContainerFactory<StreamListenerContainer> nativeObsFactory(Environment env) {
328327

329328
StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env);
330329
factory.setNativeListener(true);

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
2323
import org.springframework.beans.factory.FactoryBean;
24-
import org.springframework.retry.RetryOperations;
24+
import org.springframework.core.retry.RetryPolicy;
2525

2626
/**
2727
* Convenient base class for interceptor factories.
@@ -33,18 +33,18 @@ public abstract class AbstractRetryOperationsInterceptorFactoryBean implements F
3333

3434
private @Nullable MessageRecoverer messageRecoverer;
3535

36-
private @Nullable RetryOperations retryTemplate;
36+
private @Nullable RetryPolicy retryPolicy;
3737

38-
public void setRetryOperations(RetryOperations retryTemplate) {
39-
this.retryTemplate = retryTemplate;
38+
public void setRetryPolicy(RetryPolicy retryPolicy) {
39+
this.retryPolicy = retryPolicy;
4040
}
4141

4242
public void setMessageRecoverer(MessageRecoverer messageRecoverer) {
4343
this.messageRecoverer = messageRecoverer;
4444
}
4545

46-
protected @Nullable RetryOperations getRetryOperations() {
47-
return this.retryTemplate;
46+
protected @Nullable RetryPolicy getRetryPolicy() {
47+
return this.retryPolicy;
4848
}
4949

5050
protected @Nullable MessageRecoverer getMessageRecoverer() {

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@
2929
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint;
3030
import org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener;
3131
import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor;
32+
import org.springframework.amqp.rabbit.retry.MessageRecoveryCallback;
3233
import org.springframework.amqp.utils.JavaUtils;
3334
import org.springframework.beans.BeansException;
3435
import org.springframework.context.ApplicationContext;
3536
import org.springframework.context.ApplicationContextAware;
36-
import org.springframework.retry.RecoveryCallback;
37-
import org.springframework.retry.support.RetryTemplate;
37+
import org.springframework.core.retry.RetryTemplate;
3838
import org.springframework.util.Assert;
3939

4040
/**
@@ -58,7 +58,7 @@ public abstract class BaseRabbitListenerContainerFactory<C extends MessageListen
5858

5959
private @Nullable RetryTemplate retryTemplate;
6060

61-
private @Nullable RecoveryCallback<?> recoveryCallback;
61+
private @Nullable MessageRecoveryCallback recoveryCallback;
6262

6363
private Advice @Nullable [] adviceChain;
6464

@@ -108,22 +108,22 @@ public void setBeforeSendReplyPostProcessors(MessagePostProcessor... postProcess
108108
* Set a {@link RetryTemplate} to use when sending replies; added to each message
109109
* listener adapter.
110110
* @param retryTemplate the template.
111-
* @see #setReplyRecoveryCallback(RecoveryCallback)
111+
* @see #setReplyRecoveryCallback(MessageRecoveryCallback)
112112
* @see AbstractAdaptableMessageListener#setRetryTemplate(RetryTemplate)
113113
*/
114114
public void setRetryTemplate(RetryTemplate retryTemplate) {
115115
this.retryTemplate = retryTemplate;
116116
}
117117

118118
/**
119-
* Set a {@link RecoveryCallback} to invoke when retries are exhausted. Added to each
120-
* message listener adapter. Only used if a {@link #setRetryTemplate(RetryTemplate)
119+
* Set a {@link MessageRecoveryCallback} to invoke when retries are exhausted. Added to
120+
* each message listener adapter. Only used if a {@link #setRetryTemplate(RetryTemplate)
121121
* retryTemplate} is provided.
122122
* @param recoveryCallback the recovery callback.
123123
* @see #setRetryTemplate(RetryTemplate)
124-
* @see AbstractAdaptableMessageListener#setRecoveryCallback(RecoveryCallback)
124+
* @see AbstractAdaptableMessageListener#setRecoveryCallback(MessageRecoveryCallback)
125125
*/
126-
public void setReplyRecoveryCallback(RecoveryCallback<?> recoveryCallback) {
126+
public void setReplyRecoveryCallback(MessageRecoveryCallback recoveryCallback) {
127127
this.recoveryCallback = recoveryCallback;
128128
}
129129

0 commit comments

Comments
 (0)