Browse Source

Consistently include exceptions for previous attempts in RetryException

In RetryTemplate, if we encounter an InterruptedException while
sleeping for the configured back-off duration, we throw a
RetryException with the InterruptedException as the cause.

However, in contrast to the specification for RetryException, we do not
currently include the exceptions for previous retry attempts as
suppressed exceptions in the RetryException which is thrown in such
scenarios.

In order to comply with the documented contract for RetryException,
this commit includes exceptions for previous attempts in the
RetryException thrown for an InterruptedException as well.

Closes gh-35434
pull/35447/head
Sam Brannen 3 months ago
parent
commit
7484b9c491
  1. 23
      spring-core/src/main/java/org/springframework/core/retry/RetryException.java
  2. 4
      spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java

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

@ -22,13 +22,22 @@ import java.util.Objects;
/** /**
* Exception thrown when a {@link RetryPolicy} has been exhausted. * Exception thrown when a {@link RetryPolicy} has been exhausted.
* *
* <p>A {@code RetryException} will contain the last exception thrown by the * <p>A {@code RetryException} will typically contain the last exception thrown
* {@link Retryable} operation as the {@linkplain #getCause() cause} and any * by the {@link Retryable} operation as the {@linkplain #getCause() cause} and
* exceptions from previous attempts as {@linkplain #getSuppressed() suppressed * any exceptions from previous attempts as {@linkplain #getSuppressed() suppressed
* exceptions}. * exceptions}.
* *
* <p>However, if an {@link InterruptedException} is encountered while
* {@linkplain Thread#sleep(long) sleeping} for the current
* {@link org.springframework.util.backoff.BackOff BackOff} duration, a
* {@code RetryException} will contain the {@code InterruptedException} as the
* {@linkplain #getCause() cause} and any exceptions from previous attempts to
* invoke the {@code Retryable} operation as {@linkplain #getSuppressed()
* suppressed exceptions}.
*
* @author Mahmoud Ben Hassine * @author Mahmoud Ben Hassine
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 7.0 * @since 7.0
* @see RetryOperations * @see RetryOperations
*/ */
@ -41,7 +50,9 @@ public class RetryException extends Exception {
/** /**
* Create a new {@code RetryException} for the supplied message and cause. * Create a new {@code RetryException} for the supplied message and cause.
* @param message the detail message * @param message the detail message
* @param cause the last exception thrown by the {@link Retryable} operation * @param cause the last exception thrown by the {@link Retryable} operation,
* or an {@link InterruptedException} thrown while sleeping for the current
* {@code BackOff} duration
*/ */
public RetryException(String message, Throwable cause) { public RetryException(String message, Throwable cause) {
super(message, Objects.requireNonNull(cause, "cause must not be null")); super(message, Objects.requireNonNull(cause, "cause must not be null"));
@ -49,7 +60,9 @@ public class RetryException extends Exception {
/** /**
* Get the last exception thrown by the {@link Retryable} operation. * Get the last exception thrown by the {@link Retryable} operation, or an
* {@link InterruptedException} thrown while sleeping for the current
* {@code BackOff} duration.
*/ */
@Override @Override
public final Throwable getCause() { public final Throwable getCause() {

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

@ -164,9 +164,11 @@ public class RetryTemplate implements RetryOperations {
} }
catch (InterruptedException interruptedException) { catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new RetryException( RetryException retryException = new RetryException(
"Unable to back off for retryable operation '%s'".formatted(retryableName), "Unable to back off for retryable operation '%s'".formatted(retryableName),
interruptedException); interruptedException);
exceptions.forEach(retryException::addSuppressed);
throw retryException;
} }
logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName)); logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName));
try { try {

Loading…
Cancel
Save