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:
Sam Brannen
2025-09-08 13:47:23 +02:00
parent 13d36a51de
commit b2cdfbadf1
5 changed files with 72 additions and 6 deletions
@@ -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();
@@ -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);
}
}