Skip to content

Commit bcdf26d

Browse files
committed
Redesign RetryPolicy to directly incorporate BackOff
After experimenting with our newly introduced core retry support (RetryPolicy, RetryTemplate, etc.) and @⁠Retryable support, it became apparent that there are overlapping concerns between the current RetryPolicy and BackOff contracts. - RetryPolicy and BackOff both have stateful executions: RetryExecution and BackOffExecution. However, only one stateful execution is necessary. - FixedBackOff and ExponentialBackOff already incorporate "retry" logic in terms of max attempts, max elapsed time, etc. Thus, there is no need to duplicate such behavior in a RetryPolicy and its RetryExecution. - RetryTemplate currently accepts both a RetryPolicy and a BackOff in order to instrument the retry algorithm. However, users would probably rather focus on configuring all "retry" logic via a single mechanism. In light of the above, this commit directly incorporates BackOff in RetryPolicy as follows. - Remove the RetryExecution interface and move its shouldRetry() method to RetryPolicy, replacing the current RetryExecution start() method. - Introduce a default getBackOff() method in the RetryPolicy interface. - Introduce RetryPolicy.withDefaults() factory method. - Completely overhaul the RetryPolicy.Builder to provide support for configuring a BackOff strategy. - Remove BackOff configuration from RetryTemplate. - Revise the method signatures of callbacks in RetryListener. The collective result of these changes can be witnessed in the reworked implementation of AbstractRetryInterceptor. RetryPolicy retryPolicy = RetryPolicy.builder() .includes(spec.includes()) .excludes(spec.excludes()) .predicate(spec.predicate().forMethod(method)) .maxAttempts(spec.maxAttempts()) .delay(Duration.ofMillis(spec.delay())) .maxDelay(Duration.ofMillis(spec.maxDelay())) .jitter(Duration.ofMillis(spec.jitter())) .multiplier(spec.multiplier()) .build(); RetryTemplate retryTemplate = new RetryTemplate(retryPolicy); See gh-34716 See gh-34529 See gh-35058 Closes gh-35110
1 parent 5a6c019 commit bcdf26d

File tree

13 files changed

+990
-523
lines changed

13 files changed

+990
-523
lines changed

spring-aop/src/main/java/org/springframework/aop/retry/AbstractRetryInterceptor.java

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import org.springframework.core.retry.RetryTemplate;
3535
import org.springframework.core.retry.Retryable;
3636
import org.springframework.util.ClassUtils;
37-
import org.springframework.util.backoff.ExponentialBackOff;
3837

3938
/**
4039
* Abstract retry interceptor implementation, adapting a given
@@ -89,26 +88,17 @@ public AbstractRetryInterceptor() {
8988
}
9089
}
9190

92-
RetryTemplate retryTemplate = new RetryTemplate();
93-
94-
RetryPolicy.Builder policyBuilder = RetryPolicy.builder();
95-
for (Class<? extends Throwable> include : spec.includes()) {
96-
policyBuilder.includes(include);
97-
}
98-
for (Class<? extends Throwable> exclude : spec.excludes()) {
99-
policyBuilder.excludes(exclude);
100-
}
101-
policyBuilder.predicate(spec.predicate().forMethod(method));
102-
policyBuilder.maxAttempts(spec.maxAttempts());
103-
retryTemplate.setRetryPolicy(policyBuilder.build());
104-
105-
ExponentialBackOff backOff = new ExponentialBackOff();
106-
backOff.setInitialInterval(spec.delay());
107-
backOff.setJitter(spec.jitter());
108-
backOff.setMultiplier(spec.multiplier());
109-
backOff.setMaxInterval(spec.maxDelay());
110-
backOff.setMaxAttempts(spec.maxAttempts());
111-
retryTemplate.setBackOffPolicy(backOff);
91+
RetryPolicy retryPolicy = RetryPolicy.builder()
92+
.includes(spec.includes())
93+
.excludes(spec.excludes())
94+
.predicate(spec.predicate().forMethod(method))
95+
.maxAttempts(spec.maxAttempts())
96+
.delay(Duration.ofMillis(spec.delay()))
97+
.maxDelay(Duration.ofMillis(spec.maxDelay()))
98+
.jitter(Duration.ofMillis(spec.jitter()))
99+
.multiplier(spec.multiplier())
100+
.build();
101+
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
112102

113103
try {
114104
return retryTemplate.execute(new Retryable<>() {

spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java

Lines changed: 33 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@
1616

1717
package org.springframework.core.retry;
1818

19-
import java.time.Duration;
20-
import java.time.LocalDateTime;
2119
import java.util.Set;
2220
import java.util.StringJoiner;
2321
import java.util.function.Predicate;
2422

2523
import org.jspecify.annotations.Nullable;
2624

27-
import org.springframework.util.Assert;
25+
import org.springframework.util.backoff.BackOff;
2826

2927
/**
3028
* Default {@link RetryPolicy} created by {@link RetryPolicy.Builder}.
@@ -35,44 +33,58 @@
3533
*/
3634
class DefaultRetryPolicy implements RetryPolicy {
3735

38-
private final int maxAttempts;
39-
40-
private final @Nullable Duration maxDuration;
41-
4236
private final Set<Class<? extends Throwable>> includes;
4337

4438
private final Set<Class<? extends Throwable>> excludes;
4539

4640
private final @Nullable Predicate<Throwable> predicate;
4741

42+
private final BackOff backOff;
43+
4844

49-
DefaultRetryPolicy(int maxAttempts, @Nullable Duration maxDuration, Set<Class<? extends Throwable>> includes,
50-
Set<Class<? extends Throwable>> excludes, @Nullable Predicate<Throwable> predicate) {
5145

52-
Assert.isTrue((maxAttempts > 0 || maxDuration != null), "Max attempts or max duration must be specified");
46+
DefaultRetryPolicy(Set<Class<? extends Throwable>> includes, Set<Class<? extends Throwable>> excludes,
47+
@Nullable Predicate<Throwable> predicate, BackOff backOff) {
5348

54-
this.maxAttempts = maxAttempts;
55-
this.maxDuration = maxDuration;
5649
this.includes = includes;
5750
this.excludes = excludes;
5851
this.predicate = predicate;
52+
this.backOff = backOff;
5953
}
6054

6155

6256
@Override
63-
public RetryExecution start() {
64-
return new DefaultRetryPolicyExecution();
57+
public boolean shouldRetry(Throwable throwable) {
58+
if (!this.excludes.isEmpty()) {
59+
for (Class<? extends Throwable> excludedType : this.excludes) {
60+
if (excludedType.isInstance(throwable)) {
61+
return false;
62+
}
63+
}
64+
}
65+
if (!this.includes.isEmpty()) {
66+
boolean included = false;
67+
for (Class<? extends Throwable> includedType : this.includes) {
68+
if (includedType.isInstance(throwable)) {
69+
included = true;
70+
break;
71+
}
72+
}
73+
if (!included) {
74+
return false;
75+
}
76+
}
77+
return this.predicate == null || this.predicate.test(throwable);
78+
}
79+
80+
@Override
81+
public BackOff getBackOff() {
82+
return this.backOff;
6583
}
6684

6785
@Override
6886
public String toString() {
6987
StringJoiner result = new StringJoiner(", ", "DefaultRetryPolicy[", "]");
70-
if (this.maxAttempts > 0) {
71-
result.add("maxAttempts=" + this.maxAttempts);
72-
}
73-
if (this.maxDuration != null) {
74-
result.add("maxDuration=" + this.maxDuration.toMillis() + "ms");
75-
}
7688
if (!this.includes.isEmpty()) {
7789
result.add("includes=" + names(this.includes));
7890
}
@@ -82,6 +94,7 @@ public String toString() {
8294
if (this.predicate != null) {
8395
result.add("predicate=" + this.predicate.getClass().getSimpleName());
8496
}
97+
result.add("backOff=" + this.backOff);
8598
return result.toString();
8699
}
87100

@@ -95,73 +108,4 @@ private static String names(Set<Class<? extends Throwable>> types) {
95108
return result.toString();
96109
}
97110

98-
99-
/**
100-
* {@link RetryExecution} for {@link DefaultRetryPolicy}.
101-
*/
102-
private class DefaultRetryPolicyExecution implements RetryExecution {
103-
104-
private final LocalDateTime retryStartTime = LocalDateTime.now();
105-
106-
private int retryCount;
107-
108-
109-
@Override
110-
public boolean shouldRetry(Throwable throwable) {
111-
if (DefaultRetryPolicy.this.maxAttempts > 0 &&
112-
this.retryCount++ >= DefaultRetryPolicy.this.maxAttempts) {
113-
return false;
114-
}
115-
if (DefaultRetryPolicy.this.maxDuration != null) {
116-
Duration retryDuration = Duration.between(this.retryStartTime, LocalDateTime.now());
117-
if (retryDuration.compareTo(DefaultRetryPolicy.this.maxDuration) > 0) {
118-
return false;
119-
}
120-
}
121-
if (!DefaultRetryPolicy.this.excludes.isEmpty()) {
122-
for (Class<? extends Throwable> excludedType : DefaultRetryPolicy.this.excludes) {
123-
if (excludedType.isInstance(throwable)) {
124-
return false;
125-
}
126-
}
127-
}
128-
if (!DefaultRetryPolicy.this.includes.isEmpty()) {
129-
boolean included = false;
130-
for (Class<? extends Throwable> includedType : DefaultRetryPolicy.this.includes) {
131-
if (includedType.isInstance(throwable)) {
132-
included = true;
133-
break;
134-
}
135-
}
136-
if (!included) {
137-
return false;
138-
}
139-
}
140-
return DefaultRetryPolicy.this.predicate == null || DefaultRetryPolicy.this.predicate.test(throwable);
141-
}
142-
143-
@Override
144-
public String toString() {
145-
StringJoiner result = new StringJoiner(", ", "DefaultRetryPolicyExecution[", "]");
146-
if (DefaultRetryPolicy.this.maxAttempts > 0) {
147-
result.add("maxAttempts=" + DefaultRetryPolicy.this.maxAttempts);
148-
result.add("retryCount=" + this.retryCount);
149-
}
150-
if (DefaultRetryPolicy.this.maxDuration != null) {
151-
result.add("maxDuration=" + DefaultRetryPolicy.this.maxDuration.toMillis() + "ms");
152-
result.add("retryStartTime=" + this.retryStartTime);
153-
}
154-
if (!DefaultRetryPolicy.this.includes.isEmpty()) {
155-
result.add("includes=" + names(DefaultRetryPolicy.this.includes));
156-
}
157-
if (!DefaultRetryPolicy.this.excludes.isEmpty()) {
158-
result.add("excludes=" + names(DefaultRetryPolicy.this.excludes));
159-
}
160-
if (DefaultRetryPolicy.this.predicate != null) {
161-
result.add("predicate=" + DefaultRetryPolicy.this.predicate.getClass().getSimpleName());
162-
}
163-
return result.toString();
164-
}
165-
}
166-
167111
}

spring-core/src/main/java/org/springframework/core/retry/RetryExecution.java

Lines changed: 0 additions & 40 deletions
This file was deleted.

spring-core/src/main/java/org/springframework/core/retry/RetryListener.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,46 +21,52 @@
2121
import org.springframework.core.retry.support.CompositeRetryListener;
2222

2323
/**
24-
* An extension point that allows to inject code during key retry phases.
24+
* {@code RetryListener} defines a <em>listener</em> API for reacting to events
25+
* published during the execution of a {@link Retryable} operation.
2526
*
2627
* <p>Typically registered in a {@link RetryTemplate}, and can be composed using
2728
* a {@link CompositeRetryListener}.
2829
*
2930
* @author Mahmoud Ben Hassine
31+
* @author Sam Brannen
3032
* @since 7.0
3133
* @see CompositeRetryListener
3234
*/
3335
public interface RetryListener {
3436

3537
/**
3638
* Called before every retry attempt.
37-
* @param retryExecution the retry execution
39+
* @param retryPolicy the {@link RetryPolicy}
40+
* @param retryable the {@link Retryable} operation
3841
*/
39-
default void beforeRetry(RetryExecution retryExecution) {
42+
default void beforeRetry(RetryPolicy retryPolicy, Retryable<?> retryable) {
4043
}
4144

4245
/**
4346
* Called after the first successful retry attempt.
44-
* @param retryExecution the retry execution
45-
* @param result the result of the {@link Retryable}
47+
* @param retryPolicy the {@link RetryPolicy}
48+
* @param retryable the {@link Retryable} operation
49+
* @param result the result of the {@code Retryable} operation
4650
*/
47-
default void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) {
51+
default void onRetrySuccess(RetryPolicy retryPolicy, Retryable<?> retryable, @Nullable Object result) {
4852
}
4953

5054
/**
5155
* Called every time a retry attempt fails.
52-
* @param retryExecution the retry execution
53-
* @param throwable the exception thrown by the {@link Retryable}
56+
* @param retryPolicy the {@link RetryPolicy}
57+
* @param retryable the {@link Retryable} operation
58+
* @param throwable the exception thrown by the {@code Retryable} operation
5459
*/
55-
default void onRetryFailure(RetryExecution retryExecution, Throwable throwable) {
60+
default void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
5661
}
5762

5863
/**
5964
* Called if the {@link RetryPolicy} is exhausted.
60-
* @param retryExecution the retry execution
61-
* @param throwable the last exception thrown by the {@link Retryable}
65+
* @param retryPolicy the {@code RetryPolicy}
66+
* @param retryable the {@code Retryable} operation
67+
* @param throwable the last exception thrown by the {@link Retryable} operation
6268
*/
63-
default void onRetryPolicyExhaustion(RetryExecution retryExecution, Throwable throwable) {
69+
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
6470
}
6571

6672
}

0 commit comments

Comments
 (0)