diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java b/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java index 107c818583d..c575746b027 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java @@ -16,6 +16,8 @@ package org.springframework.core.retry; +import org.jspecify.annotations.Nullable; + import org.springframework.core.retry.support.CompositeRetryListener; /** @@ -42,7 +44,7 @@ public interface RetryListener { * @param retryExecution the retry execution * @param result the result of the {@link Retryable} */ - default void onRetrySuccess(RetryExecution retryExecution, Object result) { + default void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) { } /** 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 04e6da98123..6b806fd4d2d 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 @@ -17,14 +17,13 @@ package org.springframework.core.retry; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; -import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; import org.springframework.core.log.LogAccessor; -import org.springframework.core.retry.support.CompositeRetryListener; import org.springframework.core.retry.support.MaxRetryAttemptsPolicy; import org.springframework.util.Assert; import org.springframework.util.backoff.BackOff; @@ -48,6 +47,7 @@ import org.springframework.util.backoff.FixedBackOff; * * @author Mahmoud Ben Hassine * @author Sam Brannen + * @author Juergen Hoeller * @since 7.0 * @see RetryOperations * @see RetryPolicy @@ -57,14 +57,13 @@ import org.springframework.util.backoff.FixedBackOff; */ public class RetryTemplate implements RetryOperations { - protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); + private static final LogAccessor logger = new LogAccessor(RetryTemplate.class); - protected RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy(); + private RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy(); - protected BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1)); + private BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1)); - protected RetryListener retryListener = new RetryListener() { - }; + private RetryListener retryListener = new RetryListener() {}; /** @@ -121,7 +120,8 @@ public class RetryTemplate implements RetryOperations { /** * Set the {@link RetryListener} to use. - *

If multiple listeners are needed, use a {@link CompositeRetryListener}. + *

If multiple listeners are needed, use a + * {@link org.springframework.core.retry.support.CompositeRetryListener}. *

Defaults to a no-op implementation. * @param retryListener the retry listener to use */ @@ -158,10 +158,26 @@ public class RetryTemplate implements RetryOperations { // Retry process starts here RetryExecution retryExecution = this.retryPolicy.start(); BackOffExecution backOffExecution = this.backOffPolicy.start(); - List suppressedExceptions = new ArrayList<>(); + Deque exceptions = new ArrayDeque<>(); + exceptions.add(initialException); Throwable retryException = initialException; while (retryExecution.shouldRetry(retryException)) { + try { + long duration = backOffExecution.nextBackOff(); + if (duration == BackOffExecution.STOP) { + break; + } + logger.debug(() -> "Backing off for %dms after retryable operation '%s'" + .formatted(duration, retryableName)); + Thread.sleep(duration); + } + catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new RetryException( + "Unable to back off for retryable operation '%s'".formatted(retryableName), + interruptedException); + } logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName)); try { this.retryListener.beforeRetry(retryExecution); @@ -172,29 +188,22 @@ public class RetryTemplate implements RetryOperations { return result; } catch (Throwable currentAttemptException) { + logger.debug(() -> "Retry attempt for operation '%s' failed due to '%s'" + .formatted(retryableName, currentAttemptException)); this.retryListener.onRetryFailure(retryExecution, currentAttemptException); - try { - long duration = backOffExecution.nextBackOff(); - logger.debug(() -> "Retryable operation '%s' failed due to '%s'; backing off for %dms" - .formatted(retryableName, currentAttemptException.getMessage(), duration)); - Thread.sleep(duration); - } - catch (InterruptedException interruptedException) { - Thread.currentThread().interrupt(); - throw new RetryException( - "Unable to back off for retryable operation '%s'".formatted(retryableName), - interruptedException); - } - suppressedExceptions.add(currentAttemptException); + exceptions.add(currentAttemptException); retryException = currentAttemptException; } } + // The RetryPolicy has exhausted at this point, so we throw a RetryException with the // initial exception as the cause and remaining exceptions as suppressed exceptions. RetryException finalException = new RetryException( "Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName), - initialException); - suppressedExceptions.forEach(finalException::addSuppressed); + exceptions.removeLast()); + for (Iterator it = exceptions.descendingIterator(); it.hasNext();) { + finalException.addSuppressed(it.next()); + } this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException); throw finalException; } diff --git a/spring-core/src/main/java/org/springframework/core/retry/Retryable.java b/spring-core/src/main/java/org/springframework/core/retry/Retryable.java index fffc34dc00d..21460b4537b 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/Retryable.java +++ b/spring-core/src/main/java/org/springframework/core/retry/Retryable.java @@ -16,6 +16,8 @@ package org.springframework.core.retry; +import org.jspecify.annotations.Nullable; + /** * {@code Retryable} is a functional interface that can be used to implement any * generic block of code that can potentially be retried. @@ -36,7 +38,7 @@ public interface Retryable { * @return the result of the operation * @throws Throwable if an error occurs during the execution of the operation */ - R execute() throws Throwable; + @Nullable R execute() throws Throwable; /** * A unique, logical name for this retryable operation, used to distinguish diff --git a/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java b/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java index f97a04125b9..8f7d99fa1bd 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java +++ b/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java @@ -19,6 +19,8 @@ package org.springframework.core.retry.support; import java.util.LinkedList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.retry.RetryExecution; import org.springframework.core.retry.RetryListener; import org.springframework.core.retry.RetryTemplate; @@ -26,11 +28,10 @@ import org.springframework.util.Assert; /** * A composite implementation of the {@link RetryListener} interface. + * Delegate listeners will be called in their registration order. * *

This class is used to compose multiple listeners within a {@link RetryTemplate}. * - *

Delegate listeners will be called in their registration order. - * * @author Mahmoud Ben Hassine * @since 7.0 */ @@ -63,13 +64,14 @@ public class CompositeRetryListener implements RetryListener { this.listeners.add(listener); } + @Override public void beforeRetry(RetryExecution retryExecution) { this.listeners.forEach(retryListener -> retryListener.beforeRetry(retryExecution)); } @Override - public void onRetrySuccess(RetryExecution retryExecution, Object result) { + public void onRetrySuccess(RetryExecution retryExecution, @Nullable Object result) { this.listeners.forEach(listener -> listener.onRetrySuccess(retryExecution, result)); }