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 7b0946122c3..2dd1c1b38a4 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 @@ -29,6 +29,7 @@ import org.springframework.core.retry.support.CompositeRetryListener; * * @author Mahmoud Ben Hassine * @author Sam Brannen + * @author Juergen Hoeller * @since 7.0 * @see CompositeRetryListener */ @@ -64,9 +65,13 @@ public interface RetryListener { * Called if the {@link RetryPolicy} is exhausted. * @param retryPolicy the {@code RetryPolicy} * @param retryable the {@code Retryable} operation - * @param throwable the last exception thrown by the {@link Retryable} operation + * @param exception the resulting {@link RetryException}, including the last operation + * exception as a cause and all earlier operation exceptions as suppressed exceptions + * @see RetryException#getCause() + * @see RetryException#getSuppressed() + * @see RetryException#getRetryCount() */ - default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable retryable, Throwable throwable) { + default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable retryable, RetryException exception) { } } 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 352278df0c8..faa6b242cd5 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 @@ -90,6 +90,14 @@ public class RetryTemplate implements RetryOperations { this.retryPolicy = retryPolicy; } + /** + * Return the current {@link RetryPolicy} that is in use + * with this template. + */ + public RetryPolicy getRetryPolicy() { + return this.retryPolicy; + } + /** * Set the {@link RetryListener} to use. *

If multiple listeners are needed, use a @@ -102,6 +110,14 @@ public class RetryTemplate implements RetryOperations { this.retryListener = retryListener; } + /** + * Return the current {@link RetryListener} that is in use + * with this template. + */ + public RetryListener getRetryListener() { + return this.retryListener; + } + /** * Execute the supplied {@link Retryable} operation according to the configured @@ -176,7 +192,7 @@ public class RetryTemplate implements RetryOperations { "Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName), exceptions.removeLast()); exceptions.forEach(retryException::addSuppressed); - this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, lastException); + this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, retryException); throw retryException; } } 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 f71d94d5f8f..219ab7b605c 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 @@ -21,6 +21,7 @@ import java.util.List; import org.jspecify.annotations.Nullable; +import org.springframework.core.retry.RetryException; import org.springframework.core.retry.RetryListener; import org.springframework.core.retry.RetryPolicy; import org.springframework.core.retry.RetryTemplate; @@ -34,6 +35,7 @@ import org.springframework.util.Assert; *

This class is used to compose multiple listeners within a {@link RetryTemplate}. * * @author Mahmoud Ben Hassine + * @author Juergen Hoeller * @since 7.0 */ public class CompositeRetryListener implements RetryListener { @@ -82,8 +84,8 @@ public class CompositeRetryListener implements RetryListener { } @Override - public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable retryable, Throwable throwable) { - this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, throwable)); + 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 4e3ec1c11e1..d2e32f6c441 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 @@ -68,6 +68,12 @@ class RetryTemplateTests { retryTemplate.setRetryListener(retryListener); } + @Test + void checkRetryTemplateConfiguration() { + assertThat(retryTemplate.getRetryPolicy()).isSameAs(retryPolicy); + assertThat(retryTemplate.getRetryListener()).isSameAs(retryListener); + } + @Test void retryWithImmediateSuccess() throws Exception { AtomicInteger invocationCount = new AtomicInteger(); @@ -99,10 +105,9 @@ class RetryTemplateTests { .withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution") .withCause(exception) .satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty()) - .satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero()); + .satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero()) + .satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable)); - // RetryListener interactions: - inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception); verifyNoMoreInteractions(retryListener); } @@ -122,10 +127,9 @@ class RetryTemplateTests { .withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution") .withCause(exception) .satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty()) - .satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero()); + .satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero()) + .satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable)); - // RetryListener interactions: - inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception); verifyNoMoreInteractions(retryListener); } @@ -145,10 +149,9 @@ class RetryTemplateTests { .withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution") .withCause(exception) .satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty()) - .satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero()); + .satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero()) + .satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable)); - // RetryListener interactions: - inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception); verifyNoMoreInteractions(retryListener); } @@ -194,18 +197,19 @@ class RetryTemplateTests { assertThatExceptionOfType(RetryException.class) .isThrownBy(() -> retryTemplate.execute(retryable)) .withMessage("Retry policy for operation 'test' exhausted; aborting execution") - .withCause(new CustomException("Boom 4")); + .withCause(new CustomException("Boom 4")) + .satisfies(throwable -> { + invocationCount.set(1); + repeat(3, () -> { + inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, + new CustomException("Boom " + invocationCount.incrementAndGet())); + }); + inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable); + }); // 4 = 1 initial invocation + 3 retry attempts assertThat(invocationCount).hasValue(4); - // RetryListener interactions: - invocationCount.set(1); - repeat(3, () -> { - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); - inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, - new CustomException("Boom " + invocationCount.incrementAndGet())); - }); - inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, new CustomException("Boom 4")); verifyNoMoreInteractions(retryListener); } @@ -240,16 +244,17 @@ class RetryTemplateTests { assertThatExceptionOfType(RetryException.class) .isThrownBy(() -> retryTemplate.execute(retryable)) .withMessage("Retry policy for operation 'always fails' exhausted; aborting execution") - .withCause(exception); + .withCause(exception) + .satisfies(throwable -> { + repeat(5, () -> { + inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception); + }); + inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable); + }); // 6 = 1 initial invocation + 5 retry attempts assertThat(invocationCount).hasValue(6); - // RetryListener interactions: - repeat(5, () -> { - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); - inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception); - }); - inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception); verifyNoMoreInteractions(retryListener); } @@ -291,17 +296,17 @@ class RetryTemplateTests { suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(FileNotFoundException.class), suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class) )) - .satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2)); + .satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2)) + .satisfies(throwable -> { + repeat(2, () -> { + inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class)); + }); + inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable); + }); // 3 = 1 initial invocation + 2 retry attempts assertThat(invocationCount).hasValue(3); - // RetryListener interactions: - repeat(2, () -> { - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); - inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class)); - }); - inOrder.verify(retryListener).onRetryPolicyExhaustion( - eq(retryPolicy), eq(retryable), any(IllegalStateException.class)); verifyNoMoreInteractions(retryListener); } @@ -354,17 +359,17 @@ class RetryTemplateTests { suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class), suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class) )) - .satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2)); + .satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2)) + .satisfies(throwable -> { + repeat(2, () -> { + inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); + inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(IOException.class)); + }); + inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable); + }); // 3 = 1 initial invocation + 2 retry attempts assertThat(invocationCount).hasValue(3); - // RetryListener interactions: - repeat(2, () -> { - inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable); - inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(IOException.class)); - }); - inOrder.verify(retryListener).onRetryPolicyExhaustion( - eq(retryPolicy), eq(retryable), any(CustomFileNotFoundException.class)); verifyNoMoreInteractions(retryListener); } 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 8fac26872f9..10bb628f257 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 @@ -21,6 +21,7 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.core.retry.RetryException; import org.springframework.core.retry.RetryListener; import org.springframework.core.retry.RetryPolicy; import org.springframework.core.retry.Retryable; @@ -83,7 +84,7 @@ class CompositeRetryListenerTests { @Test void onRetryPolicyExhaustion() { - Exception exception = new Exception(); + RetryException exception = new RetryException("", new Exception()); compositeRetryListener.onRetryPolicyExhaustion(retryPolicy, retryable, exception); verify(listener1).onRetryPolicyExhaustion(retryPolicy, retryable, exception);