Skip to content

Commit 7a73925

Browse files
committed
Adapt AMQP auto-configuration to core retry semantics
This commit adapts the auto-configuration of AMQP message listeners and RabbitTemplate moving away from Spring Retry. One important change is that message listeners now only require a RetryPolicy. To make the callback explicit, two customizers have been introduced to clearly separate the scope of the customization: * RabbitTemplateRetrySettingsCustomizer for the client-side and usage of RabbitTemplate. * RabbitListenerRetrySettingsCustomizer for message listeners. Closes gh-47122
1 parent 4dc1b64 commit 7a73925

File tree

15 files changed

+543
-165
lines changed

15 files changed

+543
-165
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright 2012-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.boot.retry;
18+
19+
import java.time.Duration;
20+
import java.util.function.Function;
21+
22+
import org.jspecify.annotations.Nullable;
23+
24+
import org.springframework.boot.context.properties.PropertyMapper;
25+
import org.springframework.core.retry.RetryPolicy;
26+
import org.springframework.core.retry.RetryPolicy.Builder;
27+
28+
/**
29+
* Settings for a {@link RetryPolicy}.
30+
*
31+
* @author Stephane Nicoll
32+
* @since 4.0.0
33+
*/
34+
public final class RetryPolicySettings {
35+
36+
/**
37+
* Default number of retry attempts.
38+
*/
39+
public static final long DEFAULT_MAX_ATTEMPTS = RetryPolicy.Builder.DEFAULT_MAX_ATTEMPTS;
40+
41+
/**
42+
* Default initial delay.
43+
*/
44+
public static final Duration DEFAULT_DELAY = Duration.ofMillis(RetryPolicy.Builder.DEFAULT_DELAY);
45+
46+
/**
47+
* Default multiplier, uses a fixed delay.
48+
*/
49+
public static final double DEFAULT_MULTIPLIER = RetryPolicy.Builder.DEFAULT_MULTIPLIER;
50+
51+
/**
52+
* Default maximum delay (infinite).
53+
*/
54+
public static final Duration DEFAULT_MAX_DELAY = Duration.ofMillis(RetryPolicy.Builder.DEFAULT_MAX_DELAY);
55+
56+
private Long maxAttempts = DEFAULT_MAX_ATTEMPTS;
57+
58+
private Duration delay = DEFAULT_DELAY;
59+
60+
private @Nullable Duration jitter;
61+
62+
private Double multiplier = DEFAULT_MULTIPLIER;
63+
64+
private Duration maxDelay = DEFAULT_MAX_DELAY;
65+
66+
private @Nullable Function<Builder, RetryPolicy> factory;
67+
68+
/**
69+
* Create a {@link RetryPolicy} based on the state of this instance.
70+
* @return a {@link RetryPolicy}
71+
*/
72+
public RetryPolicy createRetryPolicy() {
73+
PropertyMapper map = PropertyMapper.get();
74+
RetryPolicy.Builder builder = RetryPolicy.builder();
75+
map.from(this::getMaxAttempts).to(builder::maxAttempts);
76+
map.from(this::getDelay).to(builder::delay);
77+
map.from(this::getJitter).to(builder::jitter);
78+
map.from(this::getMultiplier).to(builder::multiplier);
79+
map.from(this::getMaxDelay).to(builder::maxDelay);
80+
return (this.factory != null) ? this.factory.apply(builder) : builder.build();
81+
}
82+
83+
/**
84+
* Return the maximum number of retry attempts.
85+
* @return the maximum number of retry attempts
86+
* @see #DEFAULT_MAX_ATTEMPTS
87+
*/
88+
public Long getMaxAttempts() {
89+
return this.maxAttempts;
90+
}
91+
92+
/**
93+
* Specify the maximum number of retry attempts.
94+
* @param maxAttempts the max attempts (must be equal or greater than zero)
95+
*/
96+
public void setMaxAttempts(Long maxAttempts) {
97+
this.maxAttempts = maxAttempts;
98+
}
99+
100+
/**
101+
* Return the base delay after the initial invocation.
102+
* @return the base delay
103+
* @see #DEFAULT_DELAY
104+
*/
105+
public Duration getDelay() {
106+
return this.delay;
107+
}
108+
109+
/**
110+
* Specify the base delay after the initial invocation.
111+
* <p>
112+
* If a {@linkplain #getMultiplier() multiplier} is specified, this serves as the
113+
* initial delay to multiply from.
114+
* @param delay the base delay (must be greater than or equal to zero)
115+
*/
116+
public void setDelay(Duration delay) {
117+
this.delay = delay;
118+
}
119+
120+
/**
121+
* Return the jitter period to enable random retry attempts.
122+
* @return the jitter value
123+
*/
124+
public @Nullable Duration getJitter() {
125+
return this.jitter;
126+
}
127+
128+
/**
129+
* Specify a jitter period for the base retry attempt, randomly subtracted or added to
130+
* the calculated delay, resulting in a value between {@code delay - jitter} and
131+
* {@code delay + jitter} but never below the {@linkplain #getDelay() base delay} or
132+
* above the {@linkplain #getMaxDelay() max delay}.
133+
* <p>
134+
* If a {@linkplain #getMultiplier() multiplier} is specified, it is applied to the
135+
* jitter value as well.
136+
* @param jitter the jitter value (must be positive)
137+
*/
138+
public void setJitter(@Nullable Duration jitter) {
139+
this.jitter = jitter;
140+
}
141+
142+
/**
143+
* Return the value to multiply the current interval by for each attempt. The default
144+
* value, {@code 1.0}, effectively results in a fixed delay.
145+
* @return the value to multiply the current interval by for each attempt
146+
* @see #DEFAULT_MULTIPLIER
147+
*/
148+
public Double getMultiplier() {
149+
return this.multiplier;
150+
}
151+
152+
/**
153+
* Specify a multiplier for a delay for the next retry attempt.
154+
* @param multiplier value to multiply the current interval by for each attempt (must
155+
* be greater than or equal to 1)
156+
*/
157+
public void setMultiplier(Double multiplier) {
158+
this.multiplier = multiplier;
159+
}
160+
161+
/**
162+
* Return the maximum delay for any retry attempt.
163+
* @return the maximum delay
164+
*/
165+
public Duration getMaxDelay() {
166+
return this.maxDelay;
167+
}
168+
169+
/**
170+
* Specify the maximum delay for any retry attempt, limiting how far
171+
* {@linkplain #getJitter() jitter} and the {@linkplain #getMultiplier() multiplier}
172+
* can increase the {@linkplain #getDelay() delay}.
173+
* <p>
174+
* The default is unlimited.
175+
* @param maxDelay the maximum delay (must be positive)
176+
* @see #DEFAULT_MAX_DELAY
177+
*/
178+
public void setMaxDelay(Duration maxDelay) {
179+
this.maxDelay = maxDelay;
180+
}
181+
182+
/**
183+
* Set the factory to use to create the {@link RetryPolicy}, or {@code null} to use
184+
* the default. The function takes a {@link RetryPolicy.Builder} initialized with the
185+
* state of this instance that can be further configured, or ignored to restart from
186+
* scratch.
187+
* @param factory a factory to customize the retry policy.
188+
*/
189+
public void setFactory(@Nullable Function<Builder, RetryPolicy> factory) {
190+
this.factory = factory;
191+
}
192+
193+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2012-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+
/**
18+
* Support for core retry functionality.
19+
*/
20+
@NullMarked
21+
package org.springframework.boot.retry;
22+
23+
import org.jspecify.annotations.NullMarked;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2012-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.boot.retry;
18+
19+
import java.time.Duration;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.core.retry.RetryPolicy;
24+
import org.springframework.util.backoff.BackOff;
25+
import org.springframework.util.backoff.ExponentialBackOff;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.mockito.Mockito.mock;
29+
30+
/**
31+
* Tests for {@link RetryPolicySettings}.
32+
*
33+
* @author Stephane Nicoll
34+
*/
35+
class RetryPolicySettingsTests {
36+
37+
@Test
38+
void createRetryPolicyWithDefaultsMatchesBackOffDefaults() {
39+
RetryPolicy defaultRetryPolicy = RetryPolicy.builder().build();
40+
RetryPolicy retryPolicy = new RetryPolicySettings().createRetryPolicy();
41+
assertThat(retryPolicy.getBackOff()).isInstanceOf(ExponentialBackOff.class);
42+
ExponentialBackOff defaultBackOff = (ExponentialBackOff) defaultRetryPolicy.getBackOff();
43+
ExponentialBackOff backOff = (ExponentialBackOff) retryPolicy.getBackOff();
44+
assertThat(backOff.getMaxAttempts()).isEqualTo(defaultBackOff.getMaxAttempts());
45+
assertThat(backOff.getInitialInterval()).isEqualTo(defaultBackOff.getInitialInterval());
46+
assertThat(backOff.getJitter()).isEqualTo(defaultBackOff.getJitter());
47+
assertThat(backOff.getMultiplier()).isEqualTo(defaultBackOff.getMultiplier());
48+
assertThat(backOff.getMaxInterval()).isEqualTo(defaultBackOff.getMaxInterval());
49+
}
50+
51+
@Test
52+
void createRetryPolicyWithCustomAttributes() {
53+
RetryPolicySettings settings = new RetryPolicySettings();
54+
settings.setMaxAttempts(10L);
55+
settings.setDelay(Duration.ofSeconds(2));
56+
settings.setJitter(Duration.ofMillis(500));
57+
settings.setMultiplier(2.0);
58+
settings.setMaxDelay(Duration.ofSeconds(20));
59+
RetryPolicy retryPolicy = settings.createRetryPolicy();
60+
assertThat(retryPolicy.getBackOff()).isInstanceOfSatisfying(ExponentialBackOff.class, (backOff) -> {
61+
assertThat(backOff.getMaxAttempts()).isEqualTo(10);
62+
assertThat(backOff.getInitialInterval()).isEqualTo(2000);
63+
assertThat(backOff.getJitter()).isEqualTo(500);
64+
assertThat(backOff.getMultiplier()).isEqualTo(2.0);
65+
assertThat(backOff.getMaxInterval()).isEqualTo(20_000);
66+
});
67+
}
68+
69+
@Test
70+
void createRetryPolicyWithFactoryCanOverrideAttribute() {
71+
RetryPolicySettings settings = new RetryPolicySettings();
72+
settings.setDelay(Duration.ofSeconds(2));
73+
settings.setMultiplier(2.0);
74+
settings.setFactory((builder) -> builder.multiplier(3.0).build());
75+
RetryPolicy retryPolicy = settings.createRetryPolicy();
76+
assertThat(retryPolicy.getBackOff()).isInstanceOfSatisfying(ExponentialBackOff.class, (backOff) -> {
77+
assertThat(backOff.getInitialInterval()).isEqualTo(2000L);
78+
assertThat(backOff.getMultiplier()).isEqualTo(3.0);
79+
});
80+
}
81+
82+
@Test
83+
void createRetryPolicyWithFactoryCanIgnoreBuilder() {
84+
BackOff backOff = mock(BackOff.class);
85+
RetryPolicySettings settings = new RetryPolicySettings();
86+
settings.setFactory((builder) -> RetryPolicy.builder().backOff(backOff).build());
87+
RetryPolicy retryPolicy = settings.createRetryPolicy();
88+
assertThat(retryPolicy.getBackOff()).isEqualTo(backOff);
89+
}
90+
91+
}

documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ spring:
7474
----
7575

7676
Retries are disabled by default.
77-
You can also customize the javadoc:org.springframework.retry.support.RetryTemplate[] programmatically by declaring a javadoc:org.springframework.boot.autoconfigure.amqp.RabbitRetryTemplateCustomizer[] bean.
77+
You can also customize the javadoc:org.springframework.core.retry.RetryTemplate[] programmatically by declaring a javadoc:org.springframework.boot.autoconfigure.amqp.RabbitTemplateRetrySettingsCustomizer[] bean.
7878

7979
If you need to create more javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.autoconfigure.amqp.RabbitTemplateConfigurer[] bean that you can use to initialize a javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[] with the same settings as the factories used by the auto-configuration.
8080

@@ -129,7 +129,7 @@ You can enable retries to handle situations where your listener throws an except
129129
By default, javadoc:org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer[] is used, but you can define a javadoc:org.springframework.amqp.rabbit.retry.MessageRecoverer[] of your own.
130130
When retries are exhausted, the message is rejected and either dropped or routed to a dead-letter exchange if the broker is configured to do so.
131131
By default, retries are disabled.
132-
You can also customize the javadoc:org.springframework.retry.support.RetryTemplate[] programmatically by declaring a javadoc:org.springframework.boot.autoconfigure.amqp.RabbitRetryTemplateCustomizer[] bean.
132+
You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] programmatically by declaring a javadoc:org.springframework.boot.autoconfigure.amqp.RabbitListenerRetrySettingsCustomizer[] bean.
133133

134134
IMPORTANT: By default, if retries are disabled and the listener throws an exception, the delivery is retried indefinitely.
135135
You can modify this behavior in two ways: Set the `defaultRequeueRejected` property to `false` so that zero re-deliveries are attempted or throw an javadoc:org.springframework.amqp.AmqpRejectAndDontRequeueException[] to signal the message should be rejected.

module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AbstractRabbitListenerContainerFactoryConfigurer.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
import org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer;
2929
import org.springframework.amqp.support.converter.MessageConverter;
3030
import org.springframework.boot.amqp.autoconfigure.RabbitProperties.ListenerRetry;
31-
import org.springframework.retry.support.RetryTemplate;
31+
import org.springframework.boot.amqp.autoconfigure.RabbitProperties.Retry;
32+
import org.springframework.boot.retry.RetryPolicySettings;
33+
import org.springframework.core.retry.RetryPolicy;
3234
import org.springframework.util.Assert;
3335

3436
/**
@@ -46,7 +48,7 @@ public abstract class AbstractRabbitListenerContainerFactoryConfigurer<T extends
4648

4749
private @Nullable MessageRecoverer messageRecoverer;
4850

49-
private @Nullable List<RabbitRetryTemplateCustomizer> retryTemplateCustomizers;
51+
private @Nullable List<RabbitListenerRetrySettingsCustomizer> retrySettingsCustomizers;
5052

5153
private final RabbitProperties rabbitProperties;
5254

@@ -78,11 +80,12 @@ protected void setMessageRecoverer(@Nullable MessageRecoverer messageRecoverer)
7880
}
7981

8082
/**
81-
* Set the {@link RabbitRetryTemplateCustomizer} instances to use.
82-
* @param retryTemplateCustomizers the retry template customizers
83+
* Set the {@link RabbitListenerRetrySettingsCustomizer} instances to use.
84+
* @param retrySettingsCustomizers the retry settings customizers
8385
*/
84-
protected void setRetryTemplateCustomizers(@Nullable List<RabbitRetryTemplateCustomizer> retryTemplateCustomizers) {
85-
this.retryTemplateCustomizers = retryTemplateCustomizers;
86+
protected void setRetrySettingsCustomizers(
87+
@Nullable List<RabbitListenerRetrySettingsCustomizer> retrySettingsCustomizers) {
88+
this.retrySettingsCustomizers = retrySettingsCustomizers;
8689
}
8790

8891
/**
@@ -139,14 +142,22 @@ protected void configure(T factory, ConnectionFactory connectionFactory,
139142
if (retryConfig.isEnabled()) {
140143
RetryInterceptorBuilder<?, ?> builder = (retryConfig.isStateless()) ? RetryInterceptorBuilder.stateless()
141144
: RetryInterceptorBuilder.stateful();
142-
RetryTemplate retryTemplate = new RetryTemplateFactory(this.retryTemplateCustomizers)
143-
.createRetryTemplate(retryConfig, RabbitRetryTemplateCustomizer.Target.LISTENER);
144-
builder.retryOperations(retryTemplate);
145+
builder.retryPolicy(createRetryPolicy(retryConfig));
145146
MessageRecoverer recoverer = (this.messageRecoverer != null) ? this.messageRecoverer
146147
: new RejectAndDontRequeueRecoverer();
147148
builder.recoverer(recoverer);
148149
factory.setAdviceChain(builder.build());
149150
}
150151
}
151152

153+
private RetryPolicy createRetryPolicy(Retry retryProperties) {
154+
RetryPolicySettings retrySettings = retryProperties.initializeRetryPolicySettings();
155+
if (this.retrySettingsCustomizers != null) {
156+
for (RabbitListenerRetrySettingsCustomizer customizer : this.retrySettingsCustomizers) {
157+
customizer.customize(retrySettings);
158+
}
159+
}
160+
return retrySettings.createRetryPolicy();
161+
}
162+
152163
}

0 commit comments

Comments
 (0)