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 2dd1c1b38a4..36aed999201 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 @@ -53,7 +53,7 @@ public interface RetryListener { } /** - * Called every time a retry attempt fails. + * Called after every failed retry attempt. * @param retryPolicy the {@link RetryPolicy} * @param retryable the {@link Retryable} operation * @param throwable the exception thrown by the {@code Retryable} operation @@ -65,8 +65,9 @@ public interface RetryListener { * Called if the {@link RetryPolicy} is exhausted. * @param retryPolicy the {@code RetryPolicy} * @param retryable the {@code Retryable} operation - * @param exception the resulting {@link RetryException}, including the last operation - * exception as a cause and all earlier operation exceptions as suppressed exceptions + * @param exception the resulting {@link RetryException}, with the last + * exception thrown by the {@link Retryable} operation as the cause and any + * exceptions from previous attempts as suppressed exceptions * @see RetryException#getCause() * @see RetryException#getSuppressed() * @see RetryException#getRetryCount() @@ -74,4 +75,19 @@ public interface RetryListener { default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable> retryable, RetryException exception) { } + /** + * Called if an {@link InterruptedException} is encountered while + * {@linkplain Thread#sleep(long) sleeping} between retry attempts. + * @param retryPolicy the {@code RetryPolicy} + * @param retryable the {@code Retryable} operation + * @param exception the resulting {@link RetryException}, with the + * {@code InterruptedException} as the cause and any exceptions from previous + * retry attempts as suppressed exceptions + * @see RetryException#getCause() + * @see RetryException#getSuppressed() + * @see RetryException#getRetryCount() + */ + default void onRetryPolicyInterruption(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 bf55b724c67..9d374d5e467 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 @@ -168,6 +168,7 @@ public class RetryTemplate implements RetryOperations { "Unable to back off for retryable operation '%s'".formatted(retryableName), interruptedException); exceptions.forEach(retryException::addSuppressed); + this.retryListener.onRetryPolicyInterruption(this.retryPolicy, retryable, retryException); throw retryException; } logger.debug(() -> "Preparing to retry operation '%s'".formatted(retryableName)); 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 219ab7b605c..c9b16865a5b 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 @@ -29,13 +29,14 @@ import org.springframework.core.retry.Retryable; import org.springframework.util.Assert; /** - * A composite implementation of the {@link RetryListener} interface. - * Delegate listeners will be called in their registration order. + * A composite implementation of the {@link RetryListener} interface, which is + * used to compose multiple listeners within a {@link RetryTemplate}. * - *
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
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 7.0
*/
public class CompositeRetryListener implements RetryListener {
@@ -88,4 +89,9 @@ public class CompositeRetryListener implements RetryListener {
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, exception));
}
+ @Override
+ public void onRetryPolicyInterruption(RetryPolicy retryPolicy, Retryable> retryable, RetryException exception) {
+ this.listeners.forEach(listener -> listener.onRetryPolicyInterruption(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 d2e32f6c441..8e8bbaa2fcf 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
@@ -30,8 +30,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments.ArgumentSet;
import org.junit.jupiter.params.provider.FieldSource;
+import org.junit.platform.commons.util.ExceptionUtils;
import org.mockito.InOrder;
+import org.springframework.util.backoff.BackOff;
+
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
@@ -213,6 +216,36 @@ class RetryTemplateTests {
verifyNoMoreInteractions(retryListener);
}
+ @Test
+ void retryWithInterruptionDuringSleep() {
+ Exception exception = new RuntimeException("Boom!");
+ InterruptedException interruptedException = new InterruptedException();
+
+ // Simulates interruption during sleep:
+ BackOff backOff = () -> () -> {
+ throw ExceptionUtils.throwAsUncheckedException(interruptedException);
+ };
+
+ RetryPolicy retryPolicy = RetryPolicy.builder().backOff(backOff).build();
+ RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
+ retryTemplate.setRetryListener(retryListener);
+ Retryable