Skip to content

Commit 945f3fb

Browse files
committed
Revise RetryTemplate for alignment with Reactor
Exposes last exception as cause in RetryException. Applies first back-off after the initial exception. Breaks out of retry loop on BackOffExecution.STOP. Expects null result in Retryable and RetryListener. Closes gh-35057
1 parent 2aa0bad commit 945f3fb

File tree

4 files changed

+46
-31
lines changed

4 files changed

+46
-31
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.core.retry;
1818

19+
import org.jspecify.annotations.Nullable;
20+
1921
import org.springframework.core.retry.support.CompositeRetryListener;
2022

2123
/**
@@ -42,7 +44,7 @@ default void beforeRetry(RetryExecution retryExecution) {
4244
* @param retryExecution the retry execution
4345
* @param result the result of the {@link Retryable}
4446
*/
45-
default void onRetrySuccess(RetryExecution retryExecution, Object result) {
47+
default void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) {
4648
}
4749

4850
/**

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

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@
1717
package org.springframework.core.retry;
1818

1919
import java.time.Duration;
20-
import java.util.ArrayList;
21-
import java.util.List;
20+
import java.util.ArrayDeque;
21+
import java.util.Deque;
22+
import java.util.Iterator;
2223

23-
import org.apache.commons.logging.LogFactory;
2424
import org.jspecify.annotations.Nullable;
2525

2626
import org.springframework.core.log.LogAccessor;
27-
import org.springframework.core.retry.support.CompositeRetryListener;
2827
import org.springframework.core.retry.support.MaxRetryAttemptsPolicy;
2928
import org.springframework.util.Assert;
3029
import org.springframework.util.backoff.BackOff;
@@ -48,6 +47,7 @@
4847
*
4948
* @author Mahmoud Ben Hassine
5049
* @author Sam Brannen
50+
* @author Juergen Hoeller
5151
* @since 7.0
5252
* @see RetryOperations
5353
* @see RetryPolicy
@@ -57,14 +57,13 @@
5757
*/
5858
public class RetryTemplate implements RetryOperations {
5959

60-
protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass()));
60+
private static final LogAccessor logger = new LogAccessor(RetryTemplate.class);
6161

62-
protected RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy();
62+
private RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy();
6363

64-
protected BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1));
64+
private BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1));
6565

66-
protected RetryListener retryListener = new RetryListener() {
67-
};
66+
private RetryListener retryListener = new RetryListener() {};
6867

6968

7069
/**
@@ -121,7 +120,8 @@ public void setBackOffPolicy(BackOff backOffPolicy) {
121120

122121
/**
123122
* Set the {@link RetryListener} to use.
124-
* <p>If multiple listeners are needed, use a {@link CompositeRetryListener}.
123+
* <p>If multiple listeners are needed, use a
124+
* {@link org.springframework.core.retry.support.CompositeRetryListener}.
125125
* <p>Defaults to a <em>no-op</em> implementation.
126126
* @param retryListener the retry listener to use
127127
*/
@@ -158,10 +158,26 @@ public void setRetryListener(RetryListener retryListener) {
158158
// Retry process starts here
159159
RetryExecution retryExecution = this.retryPolicy.start();
160160
BackOffExecution backOffExecution = this.backOffPolicy.start();
161-
List<Throwable> suppressedExceptions = new ArrayList<>();
161+
Deque<Throwable> exceptions = new ArrayDeque<>();
162+
exceptions.add(initialException);
162163

163164
Throwable retryException = initialException;
164165
while (retryExecution.shouldRetry(retryException)) {
166+
try {
167+
long duration = backOffExecution.nextBackOff();
168+
if (duration == BackOffExecution.STOP) {
169+
break;
170+
}
171+
logger.debug(() -> "Backing off for %dms after retryable operation '%s'"
172+
.formatted(duration, retryableName));
173+
Thread.sleep(duration);
174+
}
175+
catch (InterruptedException interruptedException) {
176+
Thread.currentThread().interrupt();
177+
throw new RetryException(
178+
"Unable to back off for retryable operation '%s'".formatted(retryableName),
179+
interruptedException);
180+
}
165181
logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName));
166182
try {
167183
this.retryListener.beforeRetry(retryExecution);
@@ -172,29 +188,22 @@ public void setRetryListener(RetryListener retryListener) {
172188
return result;
173189
}
174190
catch (Throwable currentAttemptException) {
191+
logger.debug(() -> "Retry attempt for operation '%s' failed due to '%s'"
192+
.formatted(retryableName, currentAttemptException));
175193
this.retryListener.onRetryFailure(retryExecution, currentAttemptException);
176-
try {
177-
long duration = backOffExecution.nextBackOff();
178-
logger.debug(() -> "Retryable operation '%s' failed due to '%s'; backing off for %dms"
179-
.formatted(retryableName, currentAttemptException.getMessage(), duration));
180-
Thread.sleep(duration);
181-
}
182-
catch (InterruptedException interruptedException) {
183-
Thread.currentThread().interrupt();
184-
throw new RetryException(
185-
"Unable to back off for retryable operation '%s'".formatted(retryableName),
186-
interruptedException);
187-
}
188-
suppressedExceptions.add(currentAttemptException);
194+
exceptions.add(currentAttemptException);
189195
retryException = currentAttemptException;
190196
}
191197
}
198+
192199
// The RetryPolicy has exhausted at this point, so we throw a RetryException with the
193200
// initial exception as the cause and remaining exceptions as suppressed exceptions.
194201
RetryException finalException = new RetryException(
195202
"Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName),
196-
initialException);
197-
suppressedExceptions.forEach(finalException::addSuppressed);
203+
exceptions.removeLast());
204+
for (Iterator<Throwable> it = exceptions.descendingIterator(); it.hasNext();) {
205+
finalException.addSuppressed(it.next());
206+
}
198207
this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException);
199208
throw finalException;
200209
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.core.retry;
1818

19+
import org.jspecify.annotations.Nullable;
20+
1921
/**
2022
* {@code Retryable} is a functional interface that can be used to implement any
2123
* generic block of code that can potentially be retried.
@@ -36,7 +38,7 @@ public interface Retryable<R> {
3638
* @return the result of the operation
3739
* @throws Throwable if an error occurs during the execution of the operation
3840
*/
39-
R execute() throws Throwable;
41+
@Nullable R execute() throws Throwable;
4042

4143
/**
4244
* A unique, logical name for this retryable operation, used to distinguish

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,19 @@
1919
import java.util.LinkedList;
2020
import java.util.List;
2121

22+
import org.jspecify.annotations.Nullable;
23+
2224
import org.springframework.core.retry.RetryExecution;
2325
import org.springframework.core.retry.RetryListener;
2426
import org.springframework.core.retry.RetryTemplate;
2527
import org.springframework.util.Assert;
2628

2729
/**
2830
* A composite implementation of the {@link RetryListener} interface.
31+
* Delegate listeners will be called in their registration order.
2932
*
3033
* <p>This class is used to compose multiple listeners within a {@link RetryTemplate}.
3134
*
32-
* <p>Delegate listeners will be called in their registration order.
33-
*
3435
* @author Mahmoud Ben Hassine
3536
* @since 7.0
3637
*/
@@ -63,13 +64,14 @@ public void addListener(RetryListener listener) {
6364
this.listeners.add(listener);
6465
}
6566

67+
6668
@Override
6769
public void beforeRetry(RetryExecution retryExecution) {
6870
this.listeners.forEach(retryListener -> retryListener.beforeRetry(retryExecution));
6971
}
7072

7173
@Override
72-
public void onRetrySuccess(RetryExecution retryExecution, Object result) {
74+
public void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) {
7375
this.listeners.forEach(listener -> listener.onRetrySuccess(retryExecution, result));
7476
}
7577

0 commit comments

Comments
 (0)