Skip to content

Commit e7b0bfd

Browse files
committed
Introduce SpringRetryTemplateAdapter
Since Spring Retry is sunsetting, we should consider migration to Core Retry from Spring Framework. To support existing applications and smooth a little bit a migration from Spring Retry, the `SpringRetryTemplateAdapter` is introduced. This class is an extension of the `org.springframework.core.retry.RetryTemplate`, accepts fully configured `org.springframework.retry.support.RetryTemplate`, and optional `RecoveryCallback`. The `super` is initialized with "no retry" policy since all the retry logic is performed by the provided `org.springframework.retry.support.RetryTemplate` The `RabbitAdmin` was modified to demonstrate `SpringRetryTemplateAdapter`. The `RabbitAdminTests.testRetry()` is marked with `@SuppressWarnings("removal")` to prove that `SpringRetryTemplateAdapter` performs delegation properly.
1 parent fc7bd12 commit e7b0bfd

File tree

5 files changed

+150
-20
lines changed

5 files changed

+150
-20
lines changed

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.amqp.rabbit.core;
1818

1919
import java.io.IOException;
20+
import java.time.Duration;
2021
import java.util.ArrayList;
2122
import java.util.Collection;
2223
import java.util.Collections;
@@ -54,20 +55,22 @@
5455
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode;
5556
import org.springframework.amqp.rabbit.connection.ChannelProxy;
5657
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
58+
import org.springframework.amqp.rabbit.retry.SpringRetryTemplateAdapter;
5759
import org.springframework.beans.factory.BeanNameAware;
5860
import org.springframework.beans.factory.InitializingBean;
5961
import org.springframework.context.ApplicationContext;
6062
import org.springframework.context.ApplicationContextAware;
6163
import org.springframework.context.ApplicationEventPublisher;
6264
import org.springframework.context.ApplicationEventPublisherAware;
65+
import org.springframework.core.retry.RetryException;
66+
import org.springframework.core.retry.RetryPolicy;
67+
import org.springframework.core.retry.RetryTemplate;
6368
import org.springframework.core.task.SimpleAsyncTaskExecutor;
6469
import org.springframework.core.task.TaskExecutor;
6570
import org.springframework.jmx.export.annotation.ManagedOperation;
6671
import org.springframework.jmx.export.annotation.ManagedResource;
67-
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
68-
import org.springframework.retry.policy.SimpleRetryPolicy;
69-
import org.springframework.retry.support.RetryTemplate;
7072
import org.springframework.util.Assert;
73+
import org.springframework.util.ReflectionUtils;
7174
import org.springframework.util.StringUtils;
7275

7376
/**
@@ -90,9 +93,9 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat
9093

9194
private static final int DECLARE_MAX_ATTEMPTS = 5;
9295

93-
private static final int DECLARE_INITIAL_RETRY_INTERVAL = 1000;
96+
private static final Duration DECLARE_INITIAL_RETRY_DELAY = Duration.ofSeconds(1);
9497

95-
private static final int DECLARE_MAX_RETRY_INTERVAL = 5000;
98+
private static final Duration DECLARE_MAX_RETRY_DELAY = Duration.ofSeconds(5);
9699

97100
private static final double DECLARE_RETRY_MULTIPLIER = 2.0;
98101

@@ -547,9 +550,34 @@ public void setRedeclareManualDeclarations(boolean redeclareManualDeclarations)
547550
* of 5 seconds. To disable retry, set the argument to {@code null}. Note that this
548551
* retry is at the macro level - all declarations will be retried within the scope of
549552
* this template. If you supplied a {@link RabbitTemplate} that is configured with a
550-
* {@link RetryTemplate}, its template will retry each individual declaration.
553+
* {@link org.springframework.retry.support.RetryTemplate},
554+
* its template will retry each individual declaration.
551555
* @param retryTemplate the retry template.
552556
* @since 1.7.8
557+
* @deprecated since 4.0 in favor of
558+
*/
559+
@Deprecated(since = "4.0", forRemoval = true)
560+
public void setRetryTemplate(org.springframework.retry.support.@Nullable RetryTemplate retryTemplate) {
561+
if (retryTemplate != null) {
562+
setRetryTemplate(new SpringRetryTemplateAdapter(retryTemplate));
563+
}
564+
else {
565+
this.retryDisabled = true;
566+
}
567+
}
568+
569+
/**
570+
* Set a retry template for auto declarations. There is a race condition with
571+
* auto-delete, exclusive queues in that the queue might still exist for a short time,
572+
* preventing the redeclaration. The default retry configuration will try 5 times with
573+
* an exponential backOff starting at 1 second a multiplier of 2.0 and a max interval
574+
* of 5 seconds. To disable retry, set the argument to {@code null}. Note that this
575+
* retry is at the macro level - all declarations will be retried within the scope of
576+
* this template. If you supplied a {@link RabbitTemplate} that is configured with a
577+
* {@link RetryTemplate},
578+
* its template will retry each individual declaration.
579+
* @param retryTemplate the retry template.
580+
* @since 4.0
553581
*/
554582
public void setRetryTemplate(@Nullable RetryTemplate retryTemplate) {
555583
this.retryTemplate = retryTemplate;
@@ -592,13 +620,14 @@ public void afterPropertiesSet() {
592620
}
593621

594622
if (this.retryTemplate == null && !this.retryDisabled) {
595-
this.retryTemplate = new RetryTemplate();
596-
this.retryTemplate.setRetryPolicy(new SimpleRetryPolicy(DECLARE_MAX_ATTEMPTS));
597-
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
598-
backOffPolicy.setInitialInterval(DECLARE_INITIAL_RETRY_INTERVAL);
599-
backOffPolicy.setMultiplier(DECLARE_RETRY_MULTIPLIER);
600-
backOffPolicy.setMaxInterval(DECLARE_MAX_RETRY_INTERVAL);
601-
this.retryTemplate.setBackOffPolicy(backOffPolicy);
623+
RetryPolicy retryPolicy =
624+
RetryPolicy.builder()
625+
.maxAttempts(DECLARE_MAX_ATTEMPTS)
626+
.delay(DECLARE_INITIAL_RETRY_DELAY)
627+
.multiplier(DECLARE_RETRY_MULTIPLIER)
628+
.maxDelay(DECLARE_MAX_RETRY_DELAY)
629+
.build();
630+
this.retryTemplate = new RetryTemplate(retryPolicy);
602631
}
603632
if (this.connectionFactory instanceof CachingConnectionFactory ccf &&
604633
ccf.getCacheMode() == CacheMode.CONNECTION) {
@@ -623,7 +652,7 @@ public void afterPropertiesSet() {
623652
* declared for every connection. If anyone has a problem with it: use auto-startup="false".
624653
*/
625654
if (this.retryTemplate != null) {
626-
this.retryTemplate.execute(c -> {
655+
this.retryTemplate.execute(() -> {
627656
initialize();
628657
return null;
629658
});
@@ -632,6 +661,9 @@ public void afterPropertiesSet() {
632661
initialize();
633662
}
634663
}
664+
catch (RetryException ex) {
665+
ReflectionUtils.rethrowRuntimeException(ex.getCause());
666+
}
635667
finally {
636668
initializing.compareAndSet(true, false);
637669
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2025-present 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+
* https://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.rabbit.retry;
18+
19+
import java.time.Duration;
20+
21+
import org.jspecify.annotations.Nullable;
22+
23+
import org.springframework.core.retry.RetryException;
24+
import org.springframework.core.retry.RetryPolicy;
25+
import org.springframework.core.retry.Retryable;
26+
import org.springframework.retry.RecoveryCallback;
27+
import org.springframework.retry.RetryCallback;
28+
import org.springframework.retry.support.RetryTemplate;
29+
30+
/**
31+
* The {@link org.springframework.core.retry.RetryTemplate} adapter for
32+
* the {@link org.springframework.retry.support.RetryTemplate} instance.
33+
* The goal of this class is to simplify a migration path for existing retry infrastructure.
34+
* <p>
35+
* The super {@link org.springframework.core.retry.RetryTemplate} is configured with "no retry" policy
36+
* since all the retrying logic is delegated to the provided
37+
* {@link org.springframework.retry.support.RetryTemplate}.
38+
* <p>
39+
* NOTE: only stateless operations are delegated with implementation at the moment.
40+
*
41+
* @author Artem Bilan
42+
*/
43+
public class SpringRetryTemplateAdapter extends org.springframework.core.retry.RetryTemplate {
44+
45+
private final RetryTemplate retryTemplate;
46+
47+
private @Nullable RecoveryCallback<?> recoveryCallback;
48+
49+
public SpringRetryTemplateAdapter(RetryTemplate retryTemplate) {
50+
super(throwable -> false);
51+
this.retryTemplate = retryTemplate;
52+
}
53+
54+
@Override
55+
public void setRetryPolicy(RetryPolicy retryPolicy) {
56+
throw new UnsupportedOperationException(
57+
"The 'org.springframework.retry.RetryPolicy' has to be used instead " +
58+
"on the provided 'org.springframework.retry.support.RetryTemplate'");
59+
}
60+
61+
/**
62+
* Set a {@link RecoveryCallback} to be used in the
63+
* {@link org.springframework.retry.support.RetryTemplate#execute(RetryCallback, RecoveryCallback)}
64+
* delegating operation.
65+
* This is exactly a {@link RecoveryCallback} used previously in the target service alongside with a
66+
* {@link org.springframework.retry.support.RetryTemplate}.
67+
* @param recoveryCallback the {@link RecoveryCallback} to use.
68+
*/
69+
public void setRecoveryCallback(RecoveryCallback<?> recoveryCallback) {
70+
this.recoveryCallback = recoveryCallback;
71+
}
72+
73+
@Override
74+
@SuppressWarnings("unchecked")
75+
public <R> @Nullable R execute(Retryable<? extends @Nullable R> retryable) throws RetryException {
76+
SpringRetryTemplateRetryableDelegate<R> retryableDelegate =
77+
new SpringRetryTemplateRetryableDelegate<>(retryable, this.retryTemplate,
78+
(RecoveryCallback<R>) this.recoveryCallback);
79+
80+
return super.execute(retryableDelegate);
81+
}
82+
83+
private record SpringRetryTemplateRetryableDelegate<R>(Retryable<? extends @Nullable R> retryable,
84+
RetryTemplate retryTemplate,
85+
@Nullable RecoveryCallback<R> recoveryCallback)
86+
implements Retryable<R> {
87+
88+
@Override
89+
@SuppressWarnings("NullAway")
90+
public @Nullable R execute() throws Throwable {
91+
return this.retryTemplate.execute((ctx) -> this.retryable.execute(), this.recoveryCallback);
92+
}
93+
94+
}
95+
96+
}

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.amqp.rabbit.junit.BrokerTestUtils;
4949
import org.springframework.amqp.rabbit.junit.RabbitAvailable;
5050
import org.springframework.context.support.GenericApplicationContext;
51+
import org.springframework.core.retry.RetryTemplate;
5152

5253
import static org.assertj.core.api.Assertions.assertThat;
5354
import static org.assertj.core.api.Assertions.assertThatNoException;
@@ -339,7 +340,7 @@ public void testSpringWithDefaultExchangeNonImplicitBinding() {
339340
context.getBeanFactory().registerSingleton("bar", queue);
340341
Binding binding = new Binding(queueName, DestinationType.QUEUE, exchange.getName(), "test.routingKey", null);
341342
context.getBeanFactory().registerSingleton("baz", binding);
342-
this.rabbitAdmin.setRetryTemplate(null);
343+
this.rabbitAdmin.setRetryTemplate((RetryTemplate) null);
343344
this.rabbitAdmin.afterPropertiesSet();
344345

345346
try {
@@ -368,7 +369,7 @@ public void testQueueDeclareBad() {
368369
}
369370

370371
@Test
371-
public void testDeclareDelayedExchange() throws Exception {
372+
public void testDeclareDelayedExchange() {
372373
DirectExchange exchange = new DirectExchange("test.delayed.exchange");
373374
exchange.setDelayed(true);
374375
Queue queue = new Queue(UUID.randomUUID().toString(), true, false, false);

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ public void testWithinInvoke() throws Exception {
338338
}
339339

340340
@Test
341+
@SuppressWarnings("removal")
341342
public void testRetry() throws Exception {
342343
com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
343344
com.rabbitmq.client.Connection connection = mock(com.rabbitmq.client.Connection.class);

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3838
import org.springframework.context.annotation.Bean;
3939
import org.springframework.context.annotation.Configuration;
40+
import org.springframework.core.retry.RetryTemplate;
4041

4142
import static org.assertj.core.api.Assertions.assertThat;
4243
import static org.assertj.core.api.Assertions.fail;
@@ -47,7 +48,7 @@
4748
* @since 1.6
4849
*
4950
*/
50-
@RabbitAvailable(queues = { ContainerInitializationTests.TEST_MISMATCH, ContainerInitializationTests.TEST_MISMATCH2 })
51+
@RabbitAvailable(queues = {ContainerInitializationTests.TEST_MISMATCH, ContainerInitializationTests.TEST_MISMATCH2})
5152
public class ContainerInitializationTests {
5253

5354
public static final String TEST_MISMATCH = "test.mismatch";
@@ -129,7 +130,7 @@ else if (RabbitUtils.isMismatchedQueueArgs(s)) {
129130
mismatchLatch.countDown();
130131
}
131132
});
132-
return new CountDownLatch[] { cancelLatch, mismatchLatch, preventContainerRedeclareQueueLatch };
133+
return new CountDownLatch[] {cancelLatch, mismatchLatch, preventContainerRedeclareQueueLatch};
133134
}
134135

135136
@Configuration
@@ -169,7 +170,7 @@ static class Config1 extends Config0 {
169170
@Bean
170171
public RabbitAdmin admin() {
171172
RabbitAdmin admin = new RabbitAdmin(connectionFactory());
172-
admin.setRetryTemplate(null);
173+
admin.setRetryTemplate((RetryTemplate) null);
173174
return admin;
174175
}
175176

@@ -189,7 +190,6 @@ public Queue queue() {
189190
@Configuration
190191
static class Config3 extends Config2 {
191192

192-
193193
@Override
194194
public SimpleMessageListenerContainer container() {
195195
SimpleMessageListenerContainer container = super.container();

0 commit comments

Comments
 (0)