Skip to content

Commit 02af9e5

Browse files
committed
Revise core retry support
This commit constitutes a first pass over the new core retry support. - Fix code and Javadoc formatting - Polish/fix Javadoc - Fix default FixedBackOff configuration in RetryTemplate - Consistent logging in RetryTemplate - Fix listener handling in CompositeRetryListener, allowing addListener() to work - Polish tests - Ensure RetryTemplateTests do not take over 30 seconds to execute See gh-34716
1 parent 3fb4a75 commit 02af9e5

16 files changed

+181
-152
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package org.springframework.core.retry;
1818

1919
/**
20-
* Callback interface for a retryable piece of code. Used in conjunction with {@link RetryOperations}.
20+
* Callback interface for a retryable block of code.
21+
*
22+
* <p>Used in conjunction with {@link RetryOperations}.
2123
*
2224
* @author Mahmoud Ben Hassine
2325
* @since 7.0
@@ -35,11 +37,13 @@ public interface RetryCallback<R> {
3537
R run() throws Throwable;
3638

3739
/**
38-
* A unique logical name for this callback to distinguish retries around
39-
* business operations.
40-
* @return the name of the callback. Defaults to the class name.
40+
* A unique, logical name for this callback, used to distinguish retries for
41+
* different business operations.
42+
* <p>Defaults to the fully-qualified class name.
43+
* @return the name of the callback
4144
*/
4245
default String getName() {
4346
return getClass().getName();
4447
}
48+
4549
}

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.io.Serial;
2020

2121
/**
22-
* Exception class for exhausted retries.
22+
* Exception thrown when a {@link RetryPolicy} has been exhausted.
2323
*
2424
* @author Mahmoud Ben Hassine
2525
* @since 7.0
@@ -30,18 +30,19 @@ public class RetryException extends Exception {
3030
@Serial
3131
private static final long serialVersionUID = 5439915454935047936L;
3232

33+
3334
/**
34-
* Create a new exception with a message.
35-
* @param message the exception's message
35+
* Create a new {@code RetryException} for the supplied message.
36+
* @param message the detail message
3637
*/
3738
public RetryException(String message) {
3839
super(message);
3940
}
4041

4142
/**
42-
* Create a new exception with a message and a cause.
43-
* @param message the exception's message
44-
* @param cause the exception's cause
43+
* Create a new {@code RetryException} for the supplied message and cause.
44+
* @param message the detail message
45+
* @param cause the root cause
4546
*/
4647
public RetryException(String message, Throwable cause) {
4748
super(message, cause);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
package org.springframework.core.retry;
1818

1919
/**
20-
* Strategy interface to define a retry execution.
20+
* Strategy interface to define a retry execution created for a given
21+
* {@link RetryPolicy}.
2122
*
2223
* <p>Implementations may be stateful but do not need to be thread-safe.
2324
*

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ default void onRetrySuccess(RetryExecution retryExecution, Object result) {
4848
/**
4949
* Called every time a retry attempt fails.
5050
* @param retryExecution the retry execution
51-
* @param throwable the throwable thrown by the callback
51+
* @param throwable the exception thrown by the callback
5252
*/
5353
default void onRetryFailure(RetryExecution retryExecution, Throwable throwable) {
5454
}
5555

5656
/**
57-
* Called once the retry policy is exhausted.
57+
* Called if the {@link RetryPolicy} is exhausted.
5858
* @param retryExecution the retry execution
59-
* @param throwable the last throwable thrown by the callback
59+
* @param throwable the last exception thrown by the {@link RetryCallback}
6060
*/
6161
default void onRetryPolicyExhaustion(RetryExecution retryExecution, Throwable throwable) {
6262
}

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import org.jspecify.annotations.Nullable;
2020

2121
/**
22-
* Main entry point to the core retry functionality. Defines a set of retryable operations.
22+
* Interface specifying basic retry operations.
2323
*
2424
* <p>Implemented by {@link RetryTemplate}. Not often used directly, but a useful
2525
* option to enhance testability, as it can easily be mocked or stubbed.
@@ -31,15 +31,16 @@
3131
public interface RetryOperations {
3232

3333
/**
34-
* Retry the given callback (according to the retry policy configured at the implementation level)
35-
* until it succeeds or eventually throw an exception if the retry policy is exhausted.
34+
* Execute the given callback (according to the {@link RetryPolicy} configured
35+
* at the implementation level) until it succeeds, or eventually throw an
36+
* exception if the {@code RetryPolicy} is exhausted.
3637
* @param retryCallback the callback to call initially and retry if needed
37-
* @param <R> the type of the callback's result
38-
* @return the callback's result
39-
* @throws RetryException thrown if the retry policy is exhausted. All attempt exceptions
40-
* should be added as suppressed exceptions to the final exception.
38+
* @param <R> the type of the result
39+
* @return the result of the callback, if any
40+
* @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions
41+
* encountered during retry attempts should be made available as suppressed
42+
* exceptions
4143
*/
4244
<R extends @Nullable Object> R execute(RetryCallback<R> retryCallback) throws RetryException;
4345

4446
}
45-

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
public interface RetryPolicy {
2626

2727
/**
28-
* Start a new retry execution.
29-
* @return a fresh {@link RetryExecution} ready to be used
28+
* Start a new execution for this retry policy.
29+
* @return a new {@link RetryExecution}
3030
*/
3131
RetryExecution start();
3232

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

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -31,52 +31,60 @@
3131
import org.springframework.util.backoff.FixedBackOff;
3232

3333
/**
34-
* A basic implementation of {@link RetryOperations} that uses a
35-
* {@link RetryPolicy} and a {@link BackOff} to retry a
36-
* {@link RetryCallback}. By default, the callback will be called
37-
* 3 times with a fixed backoff of 1 second.
34+
* A basic implementation of {@link RetryOperations} that invokes and potentially
35+
* retries a {@link RetryCallback} based on a configured {@link RetryPolicy} and
36+
* {@link BackOff} policy.
3837
*
39-
* <p>It is also possible to register a {@link RetryListener} to intercept and inject code
40-
* during key retry phases (before a retry attempt, after a retry attempt, etc.).
38+
* <p>By default, a callback will be invoked at most 3 times with a fixed backoff
39+
* of 1 second.
40+
*
41+
* <p>A {@link RetryListener} can be {@linkplain #setRetryListener(RetryListener)
42+
* registered} to intercept and inject behavior during key retry phases (before a
43+
* retry attempt, after a retry attempt, etc.).
4144
*
4245
* <p>All retry operations performed by this class are logged at debug level,
43-
* using "org.springframework.core.retry.RetryTemplate" as log category.
46+
* using {@code "org.springframework.core.retry.RetryTemplate"} as the log category.
4447
*
4548
* @author Mahmoud Ben Hassine
49+
* @author Sam Brannen
4650
* @since 7.0
4751
* @see RetryOperations
4852
* @see RetryPolicy
4953
* @see BackOff
5054
* @see RetryListener
55+
* @see RetryCallback
5156
*/
5257
public class RetryTemplate implements RetryOperations {
5358

5459
protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
5560

5661
protected RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy();
5762

58-
protected BackOff backOffPolicy = new FixedBackOff();
63+
protected BackOff backOffPolicy = new FixedBackOff(1000, Long.MAX_VALUE);
5964

6065
protected RetryListener retryListener = new RetryListener() {
6166
};
6267

6368
/**
64-
* Create a new retry template with default settings.
69+
* Create a new {@code RetryTemplate} with maximum 3 retry attempts and a
70+
* fixed backoff of 1 second.
6571
*/
6672
public RetryTemplate() {
6773
}
6874

6975
/**
70-
* Create a new retry template with a custom {@link RetryPolicy}.
76+
* Create a new {@code RetryTemplate} with a custom {@link RetryPolicy} and a
77+
* fixed backoff of 1 second.
7178
* @param retryPolicy the retry policy to use
7279
*/
7380
public RetryTemplate(RetryPolicy retryPolicy) {
74-
Assert.notNull(retryPolicy, "Retry policy must not be null");
81+
Assert.notNull(retryPolicy, "RetryPolicy must not be null");
7582
this.retryPolicy = retryPolicy;
7683
}
7784

7885
/**
79-
* Create a new retry template with a custom {@link RetryPolicy} and {@link BackOff}.
86+
* Create a new {@code RetryTemplate} with a custom {@link RetryPolicy} and
87+
* {@link BackOff} policy.
8088
* @param retryPolicy the retry policy to use
8189
* @param backOffPolicy the backoff policy to use
8290
*/
@@ -87,88 +95,98 @@ public RetryTemplate(RetryPolicy retryPolicy, BackOff backOffPolicy) {
8795
}
8896

8997
/**
90-
* Set the {@link RetryPolicy} to use. Defaults to <code>MaxAttemptsRetryPolicy()</code>.
91-
* @param retryPolicy the retry policy to use. Must not be <code>null</code>.
98+
* Set the {@link RetryPolicy} to use.
99+
* <p>Defaults to {@code new MaxRetryAttemptsPolicy()}.
100+
* @param retryPolicy the retry policy to use
101+
* @see MaxRetryAttemptsPolicy
92102
*/
93103
public void setRetryPolicy(RetryPolicy retryPolicy) {
94104
Assert.notNull(retryPolicy, "Retry policy must not be null");
95105
this.retryPolicy = retryPolicy;
96106
}
97107

98108
/**
99-
* Set the {@link BackOff} to use. Defaults to <code>FixedBackOffPolicy(Duration.ofSeconds(1))</code>.
100-
* @param backOffPolicy the backoff policy to use. Must not be <code>null</code>.
109+
* Set the {@link BackOff} policy to use.
110+
* <p>Defaults to {@code new FixedBackOff(1000, Long.MAX_VALUE))}.
111+
* @param backOffPolicy the backoff policy to use
112+
* @see FixedBackOff
101113
*/
102114
public void setBackOffPolicy(BackOff backOffPolicy) {
103115
Assert.notNull(backOffPolicy, "BackOff policy must not be null");
104116
this.backOffPolicy = backOffPolicy;
105117
}
106118

107119
/**
108-
* Set the {@link RetryListener} to use. Defaults to a <code>NoOp</code> implementation.
109-
* If multiple listeners are needed, use a {@link CompositeRetryListener}.
110-
* @param retryListener the retry listener to use. Must not be <code>null</code>.
120+
* Set the {@link RetryListener} to use.
121+
* <p>If multiple listeners are needed, use a {@link CompositeRetryListener}.
122+
* <p>Defaults to a <em>no-op</em> implementation.
123+
* @param retryListener the retry listener to use
111124
*/
112125
public void setRetryListener(RetryListener retryListener) {
113126
Assert.notNull(retryListener, "Retry listener must not be null");
114127
this.retryListener = retryListener;
115128
}
116129

117130
/**
118-
* Call the retry callback according to the configured retry and backoff policies.
119-
* If the callback succeeds, its result is returned. Otherwise, a {@link RetryException}
120-
* will be thrown to the caller having all attempt exceptions as suppressed exceptions.
131+
* Execute the supplied {@link RetryCallback} according to the configured
132+
* retry and backoff policies.
133+
* <p>If the callback succeeds, its result will be returned. Otherwise, a
134+
* {@link RetryException} will be thrown to the caller.
121135
* @param retryCallback the callback to call initially and retry if needed
122136
* @param <R> the type of the result
123-
* @return the result of the callback if any
124-
* @throws RetryException thrown if the retry policy is exhausted. All attempt exceptions
125-
* are added as suppressed exceptions to the final exception.
137+
* @return the result of the callback, if any
138+
* @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions
139+
* encountered during retry attempts are available as suppressed exceptions
126140
*/
127141
@Override
128142
public <R extends @Nullable Object> R execute(RetryCallback<R> retryCallback) throws RetryException {
129-
Assert.notNull(retryCallback, "Retry Callback must not be null");
130143
String callbackName = retryCallback.getName();
131-
// initial attempt
144+
// Initial attempt
132145
try {
133-
logger.debug(() -> "About to execute callback '" + callbackName + "'");
146+
logger.debug(() -> "Preparing to execute callback '" + callbackName + "'");
134147
R result = retryCallback.run();
135-
logger.debug(() -> "Callback '" + callbackName + "' executed successfully");
148+
logger.debug(() -> "Callback '" + callbackName + "' completed successfully");
136149
return result;
137150
}
138151
catch (Throwable initialException) {
139-
logger.debug(initialException, () -> "Execution of callback '" + callbackName + "' failed, initiating the retry process");
140-
// retry process starts here
152+
logger.debug(initialException,
153+
() -> "Execution of callback '" + callbackName + "' failed; initiating the retry process");
154+
// Retry process starts here
141155
RetryExecution retryExecution = this.retryPolicy.start();
142156
BackOffExecution backOffExecution = this.backOffPolicy.start();
143157
List<Throwable> suppressedExceptions = new ArrayList<>();
144158

145159
Throwable retryException = initialException;
146160
while (retryExecution.shouldRetry(retryException)) {
147-
logger.debug(() -> "About to retry callback '" + callbackName + "'");
161+
logger.debug(() -> "Preparing to retry callback '" + callbackName + "'");
148162
try {
149163
this.retryListener.beforeRetry(retryExecution);
150164
R result = retryCallback.run();
151165
this.retryListener.onRetrySuccess(retryExecution, result);
152-
logger.debug(() -> "Callback '" + callbackName + "' retried successfully");
166+
logger.debug(() -> "Callback '" + callbackName + "' completed successfully after retry");
153167
return result;
154168
}
155169
catch (Throwable currentAttemptException) {
156170
this.retryListener.onRetryFailure(retryExecution, currentAttemptException);
157171
try {
158172
long duration = backOffExecution.nextBackOff();
159-
logger.debug(() -> "Retry callback '" + callbackName + "' failed for " + currentAttemptException.getMessage() + ", backing off for " + duration + "ms");
173+
logger.debug(() -> "Retry callback '" + callbackName + "' failed due to '" +
174+
currentAttemptException.getMessage() + "'; backing off for " + duration + "ms");
160175
Thread.sleep(duration);
161176
}
162177
catch (InterruptedException interruptedException) {
163178
Thread.currentThread().interrupt();
164-
throw new RetryException("Unable to backoff for retry callback '" + callbackName + "'", interruptedException);
179+
throw new RetryException("Unable to back off for retry callback '" + callbackName + "'",
180+
interruptedException);
165181
}
166182
suppressedExceptions.add(currentAttemptException);
167183
retryException = currentAttemptException;
168184
}
169185
}
170-
// retry policy exhausted at this point, throwing a RetryException with the initial exception as cause and remaining attempts exceptions as suppressed
171-
RetryException finalException = new RetryException("Retry policy for callback '" + callbackName + "' exhausted, aborting execution", initialException);
186+
// The RetryPolicy has exhausted at this point, so we throw a RetryException with the
187+
// initial exception as the cause and remaining exceptions as suppressed exceptions.
188+
RetryException finalException = new RetryException("Retry policy for callback '" + callbackName +
189+
"' exhausted; aborting execution", initialException);
172190
suppressedExceptions.forEach(finalException::addSuppressed);
173191
this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException);
174192
throw finalException;

spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
import org.springframework.util.Assert;
2626

2727
/**
28-
* A composite implementation of the {@link RetryListener} interface. This class
29-
* is used to compose multiple listeners within a {@link RetryTemplate}.
28+
* A composite implementation of the {@link RetryListener} interface.
29+
*
30+
* <p>This class is used to compose multiple listeners within a {@link RetryTemplate}.
3031
*
3132
* <p>Delegate listeners will be called in their registration order.
3233
*
@@ -35,30 +36,30 @@
3536
*/
3637
public class CompositeRetryListener implements RetryListener {
3738

38-
private final List<RetryListener> listeners;
39+
private final List<RetryListener> listeners = new LinkedList<>();
40+
3941

4042
/**
41-
* Create a new {@link CompositeRetryListener}.
43+
* Create a new {@code CompositeRetryListener}.
4244
*/
4345
public CompositeRetryListener() {
44-
this.listeners = new LinkedList<>();
4546
}
4647

4748
/**
48-
* Create a new {@link CompositeRetryListener} with a list of delegates.
49-
* @param listeners the delegate listeners to register. Must not be empty.
49+
* Create a new {@code CompositeRetryListener} with the supplied list of
50+
* delegates.
51+
* @param listeners the list of delegate listeners to register; must not be empty
5052
*/
5153
public CompositeRetryListener(List<RetryListener> listeners) {
5254
Assert.notEmpty(listeners, "RetryListener List must not be empty");
53-
this.listeners = List.copyOf(listeners);
55+
this.listeners.addAll(listeners);
5456
}
5557

5658
/**
5759
* Add a new listener to the list of delegates.
58-
* @param listener the listener to add. Must not be <code>null</code>.
60+
* @param listener the listener to add
5961
*/
6062
public void addListener(RetryListener listener) {
61-
Assert.notNull(listener, "Retry listener must not be null");
6263
this.listeners.add(listener);
6364
}
6465

0 commit comments

Comments
 (0)