mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-25 16:29:28 +00:00
Introduce onRetryPolicyInterruption() callback in RetryListener
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, prior to this commit, that RetryException propagated to the caller without notifying the registered RetryListener. To address that, this commit introduces a new onRetryPolicyInterruption() callback in RetryListener as a companion to the existing onRetryPolicyExhaustion() callback. Closes gh-35442
This commit is contained in:
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
+9
-3
@@ -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}.
|
||||
*
|
||||
* <p>This class is used to compose multiple listeners within a {@link RetryTemplate}.
|
||||
* <p>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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> retryable = () -> {
|
||||
throw exception;
|
||||
};
|
||||
|
||||
assertThatExceptionOfType(RetryException.class)
|
||||
.isThrownBy(() -> retryTemplate.execute(retryable))
|
||||
.withMessageMatching("Unable to back off for retryable operation '.+?'")
|
||||
.withCause(interruptedException)
|
||||
.satisfies(throwable -> assertThat(throwable.getSuppressed()).containsExactly(exception))
|
||||
// TODO Fix retry count for InterruptedException scenario.
|
||||
// Retry count should actually be 0.
|
||||
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(1))
|
||||
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyInterruption(retryPolicy, retryable, throwable));
|
||||
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryWithFailingRetryableAndMultiplePredicates() {
|
||||
var invocationCount = new AtomicInteger();
|
||||
|
||||
+10
@@ -92,4 +92,14 @@ class CompositeRetryListenerTests {
|
||||
verify(listener3).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onRetryPolicyInterruption() {
|
||||
RetryException exception = new RetryException("", new Exception());
|
||||
compositeRetryListener.onRetryPolicyInterruption(retryPolicy, retryable, exception);
|
||||
|
||||
verify(listener1).onRetryPolicyInterruption(retryPolicy, retryable, exception);
|
||||
verify(listener2).onRetryPolicyInterruption(retryPolicy, retryable, exception);
|
||||
verify(listener3).onRetryPolicyInterruption(retryPolicy, retryable, exception);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user