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 863bc28fe70..46e819dc6b8 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 @@ -33,12 +33,47 @@ import org.jspecify.annotations.Nullable; */ public interface RetryListener { - // Interception callbacks for retry attempts (not covering the initial invocation) + /** + * Called after every attempt, including the initial invocation. + *
The success of the attempt can be checked via {@link RetryState#isSuccessful()}; + * if not successful, the current exception can be introspected via + * {@link RetryState#getLastException()}. + * @param retryPolicy the {@link RetryPolicy} + * @param retryable the {@link Retryable} operation + * @param retryState the current state of retry processing + * (this is a live instance reflecting the current state; not intended to be stored) + * @since 7.0.2 + * @see RetryTemplate#execute(Retryable) + * @see RetryState#isSuccessful() + * @see RetryState#getLastException() + * @see RetryState#getRetryCount() + */ + default void onRetryableExecution(RetryPolicy retryPolicy, Retryable> retryable, RetryState retryState) { + } /** * Called before every retry attempt. + *
For a corresponding callback after every retry attempt, consider + * {@link #onRetryableExecution(RetryPolicy, Retryable, RetryState)}, ignoring the + * initial attempt through a corresponding {@link RetryState#getRetryCount()} check. * @param retryPolicy the {@link RetryPolicy} * @param retryable the {@link Retryable} operation + * @param retryState the current state of retry processing + * (this is a live instance reflecting the current state; not intended to be stored) + * @since 7.0.4 + * @see #onRetryableExecution(RetryPolicy, Retryable, RetryState) + */ + default void beforeRetry(RetryPolicy retryPolicy, Retryable> retryable, RetryState retryState) { + beforeRetry(retryPolicy, retryable); + } + + /** + * Called before every retry attempt. + *
Called by {@link #beforeRetry(RetryPolicy, Retryable, RetryState)}. + * Implement either this reduced method or that full-argument method, not both. + * @param retryPolicy the {@link RetryPolicy} + * @param retryable the {@link Retryable} operation + * @see #beforeRetry(RetryPolicy, Retryable, RetryState) */ default void beforeRetry(RetryPolicy retryPolicy, Retryable> retryable) { } @@ -61,27 +96,6 @@ public interface RetryListener { default void onRetryFailure(RetryPolicy retryPolicy, Retryable> retryable, Throwable throwable) { } - - // Execution callbacks for all invocation attempts and terminal scenarios - - /** - * Called after every attempt, including the initial invocation. - *
The success of the attempt can be checked via {@link RetryState#isSuccessful()}; - * if not successful, the current exception can be introspected via - * {@link RetryState#getLastException()}. - * @param retryPolicy the {@link RetryPolicy} - * @param retryable the {@link Retryable} operation - * @param retryState the current state of retry processing - * (this is a live instance reflecting the current state; not intended to be stored) - * @since 7.0.2 - * @see RetryTemplate#execute(Retryable) - * @see RetryState#isSuccessful() - * @see RetryState#getLastException() - * @see RetryState#getRetryCount() - */ - default void onRetryableExecution(RetryPolicy retryPolicy, Retryable> retryable, RetryState retryState) { - } - /** * Called if the {@link RetryPolicy} is exhausted. * @param retryPolicy the {@code RetryPolicy} 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 165989a6262..f0485c4591e 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 @@ -169,7 +169,7 @@ public class RetryTemplate implements RetryOperations { logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName)); retryState.increaseRetryCount(); - this.retryListener.beforeRetry(this.retryPolicy, retryable); + this.retryListener.beforeRetry(this.retryPolicy, retryable, retryState); try { result = retryable.execute(); } 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 33e8693af1d..58ceb33aa44 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 @@ -72,8 +72,13 @@ public class CompositeRetryListener implements RetryListener { @Override - public void beforeRetry(RetryPolicy retryPolicy, Retryable> retryable) { - this.listeners.forEach(retryListener -> retryListener.beforeRetry(retryPolicy, retryable)); + public void onRetryableExecution(RetryPolicy retryPolicy, Retryable> retryable, RetryState retryState) { + this.listeners.forEach(listener -> listener.onRetryableExecution(retryPolicy, retryable, retryState)); + } + + @Override + public void beforeRetry(RetryPolicy retryPolicy, Retryable> retryable, RetryState retryState) { + this.listeners.forEach(retryListener -> retryListener.beforeRetry(retryPolicy, retryable, retryState)); } @Override @@ -86,11 +91,6 @@ public class CompositeRetryListener implements RetryListener { this.listeners.forEach(listener -> listener.onRetryFailure(retryPolicy, retryable, throwable)); } - @Override - public void onRetryableExecution(RetryPolicy retryPolicy, Retryable> retryable, RetryState retryState) { - this.listeners.forEach(listener -> listener.onRetryableExecution(retryPolicy, retryable, retryState)); - } - @Override public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable> retryable, RetryException exception) { this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, exception)); diff --git a/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java index 189a8bac8bd..a58e366c503 100644 --- a/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java +++ b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java @@ -191,10 +191,10 @@ class RetryTemplateTests { // RetryListener interactions: inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class)); - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, new CustomException("Boom 2")); inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class)); - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class)); inOrder.verify(retryListener).onRetrySuccess(retryPolicy, retryable, "finally succeeded"); inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), argThat(state -> state.isSuccessful() && state.getRetryCount() == 2)); @@ -225,7 +225,7 @@ class RetryTemplateTests { var counter = new AtomicInteger(1); repeat(3, () -> { inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class)); - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, new CustomException("Boom " + counter.incrementAndGet())); }); @@ -303,7 +303,7 @@ class RetryTemplateTests { .satisfies(throwable -> { repeat(5, () -> { inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class)); - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception); }); inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), @@ -357,7 +357,7 @@ class RetryTemplateTests { .satisfies(throwable -> { repeat(2, () -> { inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class)); - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class)); }); inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), @@ -422,10 +422,10 @@ class RetryTemplateTests { .satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2)) .satisfies(throwable -> { inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class)); - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(RuntimeException.class)); inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class)); - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(CustomFileNotFoundException.class)); inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), argThat(state -> !state.isSuccessful() && state.getRetryCount() == 2)); @@ -475,7 +475,8 @@ class RetryTemplateTests { argThat(r -> r.getName().equals(retryable.getClass().getName())), any(RetryState.class)); inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), - argThat(r -> r.getName().equals(retryable.getClass().getName()))); + argThat(r -> r.getName().equals(retryable.getClass().getName())), + any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), argThat(r -> r.getName().equals(retryable.getClass().getName())), eq(new CustomException("Boom 2"))); @@ -483,7 +484,8 @@ class RetryTemplateTests { argThat(r -> r.getName().equals(retryable.getClass().getName())), any(RetryState.class)); inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), - argThat(r -> r.getName().equals(retryable.getClass().getName()))); + argThat(r -> r.getName().equals(retryable.getClass().getName())), + any(RetryState.class)); inOrder.verify(retryListener).onRetrySuccess(eq(retryPolicy), argThat(r -> r.getName().equals(retryable.getClass().getName())), eq("finally succeeded")); @@ -511,7 +513,8 @@ class RetryTemplateTests { argThat(r -> r.getName().equals(retryable.getClass().getName())), any(RetryState.class)); inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), - argThat(r -> r.getName().equals(retryable.getClass().getName()))); + argThat(r -> r.getName().equals(retryable.getClass().getName())), + any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), argThat(r -> r.getName().equals(retryable.getClass().getName())), eq(new CustomException("Boom " + counter.incrementAndGet()))); @@ -564,7 +567,8 @@ class RetryTemplateTests { argThat(r -> r.getName().equals(retryable.getClass().getName())), any(RetryState.class)); inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), - argThat(r -> r.getName().equals(retryable.getClass().getName()))); + argThat(r -> r.getName().equals(retryable.getClass().getName())), + any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), argThat(r -> r.getName().equals(retryable.getClass().getName())), eq(new CustomException("Boom 2"))); @@ -572,7 +576,8 @@ class RetryTemplateTests { argThat(r -> r.getName().equals(retryable.getClass().getName())), any(RetryState.class)); inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), - argThat(r -> r.getName().equals(retryable.getClass().getName()))); + argThat(r -> r.getName().equals(retryable.getClass().getName())), + any(RetryState.class)); inOrder.verify(retryListener).onRetrySuccess(eq(retryPolicy), argThat(r -> r.getName().equals(retryable.getClass().getName())), eq(null)); @@ -600,7 +605,8 @@ class RetryTemplateTests { argThat(r -> r.getName().equals(retryable.getClass().getName())), any(RetryState.class)); inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), - argThat(r -> r.getName().equals(retryable.getClass().getName()))); + argThat(r -> r.getName().equals(retryable.getClass().getName())), + any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), argThat(r -> r.getName().equals(retryable.getClass().getName())), eq(new CustomException("Boom " + counter.incrementAndGet()))); @@ -758,8 +764,8 @@ class RetryTemplateTests { .withMessageMatching("Retry policy for operation '.+?' exceeded timeout \\(20ms\\); aborting execution") .withCause(new CustomException("Boom 2")) .satisfies(throwable -> { - inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any()); - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class)); + inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, new CustomException("Boom 2")); inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), argThat(state -> !state.isSuccessful() && state.getRetryCount() == 1)); @@ -797,8 +803,8 @@ class RetryTemplateTests { .satisfies(throwable -> { var counter = new AtomicInteger(1); repeat(2, () -> { - inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any()); - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy), eq(retryable), any(RetryState.class)); + inOrder.verify(retryListener).beforeRetry(eq(retryPolicy), eq(retryable), any(RetryState.class)); inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, new CustomException("Boom " + counter.incrementAndGet())); }); diff --git a/spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java b/spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java index 08591b6a765..95a409ab114 100644 --- a/spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java +++ b/spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java @@ -54,12 +54,23 @@ class CompositeRetryListenerTests { } @Test - void beforeRetry() { - compositeRetryListener.beforeRetry(retryPolicy, retryable); + void onRetryableExecution() { + RetryException exception = new RetryException("", new Exception()); + compositeRetryListener.onRetryableExecution(retryPolicy, retryable, exception); - verify(listener1).beforeRetry(retryPolicy, retryable); - verify(listener2).beforeRetry(retryPolicy, retryable); - verify(listener3).beforeRetry(retryPolicy, retryable); + verify(listener1).onRetryableExecution(retryPolicy, retryable, exception); + verify(listener2).onRetryableExecution(retryPolicy, retryable, exception); + verify(listener3).onRetryableExecution(retryPolicy, retryable, exception); + } + + @Test + void beforeRetry() { + RetryException exception = new RetryException("", new Exception()); + compositeRetryListener.beforeRetry(retryPolicy, retryable, exception); + + verify(listener1).beforeRetry(retryPolicy, retryable, exception); + verify(listener2).beforeRetry(retryPolicy, retryable, exception); + verify(listener3).beforeRetry(retryPolicy, retryable, exception); } @Test @@ -82,16 +93,6 @@ class CompositeRetryListenerTests { verify(listener3).onRetryFailure(retryPolicy, retryable, exception); } - @Test - void onRetryableExecution() { - RetryException exception = new RetryException("", new Exception()); - compositeRetryListener.onRetryableExecution(retryPolicy, retryable, exception); - - verify(listener1).onRetryableExecution(retryPolicy, retryable, exception); - verify(listener2).onRetryableExecution(retryPolicy, retryable, exception); - verify(listener3).onRetryableExecution(retryPolicy, retryable, exception); - } - @Test void onRetryPolicyExhaustion() { RetryException exception = new RetryException("", new Exception());