From 7484b9c49173a2da9d22ea00f2298ab4ec394e37 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 6 Sep 2025 16:39:22 +0200 Subject: [PATCH] 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 --- .../core/retry/RetryException.java | 23 +++++++++++++++---- .../core/retry/RetryTemplate.java | 4 +++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryException.java b/spring-core/src/main/java/org/springframework/core/retry/RetryException.java index eef16f68ef1..9a862d543d2 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/RetryException.java +++ b/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. * - *

A {@code RetryException} will contain the last exception thrown by the - * {@link Retryable} operation as the {@linkplain #getCause() cause} and any - * exceptions from previous attempts as {@linkplain #getSuppressed() suppressed + *

A {@code RetryException} will typically contain the last exception thrown + * by the {@link Retryable} operation as the {@linkplain #getCause() cause} and + * any exceptions from previous attempts as {@linkplain #getSuppressed() suppressed * exceptions}. * + *

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 Juergen Hoeller + * @author Sam Brannen * @since 7.0 * @see RetryOperations */ @@ -41,7 +50,9 @@ public class RetryException extends Exception { /** * Create a new {@code RetryException} for the supplied message and cause. * @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) { 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 public final Throwable getCause() { diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java index 74be5041832..bf55b724c67 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java @@ -164,9 +164,11 @@ public class RetryTemplate implements RetryOperations { } catch (InterruptedException interruptedException) { Thread.currentThread().interrupt(); - throw new RetryException( + RetryException retryException = new RetryException( "Unable to back off for retryable operation '%s'".formatted(retryableName), interruptedException); + exceptions.forEach(retryException::addSuppressed); + throw retryException; } logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName)); try {