Introduce invoke(Supplier) with last original RuntimeException propagated

Closes gh-36052
This commit is contained in:
Juergen Hoeller
2025-12-20 15:20:58 +01:00
parent 89ca8e6976
commit faa17abdae
3 changed files with 158 additions and 32 deletions
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.BeforeEach;
@@ -79,7 +80,7 @@ class RetryTemplateTests {
}
@Test
void retryWithImmediateSuccess() throws Exception {
void retryableWithImmediateSuccess() throws Exception {
AtomicInteger invocationCount = new AtomicInteger();
Retryable<String> retryable = () -> {
invocationCount.incrementAndGet();
@@ -97,7 +98,7 @@ class RetryTemplateTests {
}
@Test
void retryWithInitialFailureAndZeroRetriesRetryPolicy() {
void retryableWithInitialFailureAndZeroRetriesRetryPolicy() {
RetryPolicy retryPolicy = throwable -> false; // Zero retries
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
retryTemplate.setRetryListener(retryListener);
@@ -121,9 +122,8 @@ class RetryTemplateTests {
verifyNoMoreInteractions(retryListener);
}
@Test
void retryWithInitialFailureAndZeroRetriesFixedBackOffPolicy() {
void retryableWithInitialFailureAndZeroRetriesFixedBackOffPolicy() {
RetryPolicy retryPolicy = RetryPolicy.withMaxRetries(0);
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
@@ -149,7 +149,7 @@ class RetryTemplateTests {
}
@Test
void retryWithInitialFailureAndZeroRetriesBackOffPolicyFromBuilder() {
void retryableWithInitialFailureAndZeroRetriesBackOffPolicyFromBuilder() {
RetryPolicy retryPolicy = RetryPolicy.builder().maxRetries(0).build();
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
@@ -175,7 +175,7 @@ class RetryTemplateTests {
}
@Test
void retryWithSuccessAfterInitialFailures() throws Exception {
void retryableWithSuccessAfterInitialFailures() throws Exception {
AtomicInteger invocationCount = new AtomicInteger();
Retryable<String> retryable = () -> {
if (invocationCount.incrementAndGet() <= 2) {
@@ -201,7 +201,7 @@ class RetryTemplateTests {
}
@Test
void retryWithExhaustedPolicy() {
void retryableWithExhaustedPolicy() {
var invocationCount = new AtomicInteger();
var retryable = new Retryable<>() {
@@ -239,7 +239,7 @@ class RetryTemplateTests {
}
@Test
void retryWithInterruptionDuringSleep() {
void retryableWithInterruptionDuringSleep() {
Exception exception = new RuntimeException("Boom!");
InterruptedException interruptedException = new InterruptedException();
@@ -269,7 +269,7 @@ class RetryTemplateTests {
}
@Test
void retryWithFailingRetryableAndMultiplePredicates() {
void retryableWithFailingRetryableAndMultiplePredicates() {
var invocationCount = new AtomicInteger();
var exception = new NumberFormatException("Boom!");
@@ -316,7 +316,7 @@ class RetryTemplateTests {
}
@Test
void retryWithExceptionIncludes() {
void retryableWithExceptionIncludes() {
var invocationCount = new AtomicInteger();
var retryable = new Retryable<>() {
@@ -387,7 +387,7 @@ class RetryTemplateTests {
@ParameterizedTest
@FieldSource("includesAndExcludesRetryPolicies")
void retryWithExceptionIncludesAndExcludes(RetryPolicy retryPolicy) {
void retryableWithExceptionIncludesAndExcludes(RetryPolicy retryPolicy) {
retryTemplate.setRetryPolicy(retryPolicy);
var invocationCount = new AtomicInteger();
@@ -436,12 +436,105 @@ class RetryTemplateTests {
verifyNoMoreInteractions(retryListener);
}
@Test
void supplierWithImmediateSuccess() {
AtomicInteger invocationCount = new AtomicInteger();
Supplier<String> retryable = () -> {
invocationCount.incrementAndGet();
return "always succeeds";
};
assertThat(invocationCount).hasValue(0);
assertThat(retryTemplate.invoke(retryable)).isEqualTo("always succeeds");
assertThat(invocationCount).hasValue(1);
// RetryListener interactions:
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
argThat(state -> state.isSuccessful() && state.getRetryCount() == 0));
verifyNoMoreInteractions(retryListener);
}
@Test
void supplierWithSuccessAfterInitialFailures() {
AtomicInteger invocationCount = new AtomicInteger();
Supplier<String> retryable = () -> {
if (invocationCount.incrementAndGet() <= 2) {
throw new CustomException("Boom " + invocationCount.get());
}
return "finally succeeded";
};
assertThat(invocationCount).hasValue(0);
assertThat(retryTemplate.invoke(retryable)).isEqualTo("finally succeeded");
assertThat(invocationCount).hasValue(3);
// RetryListener interactions:
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
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())));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
eq(new CustomException("Boom 2")));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
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())));
inOrder.verify(retryListener).onRetrySuccess(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
eq("finally succeeded"));
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
argThat(state -> state.isSuccessful() && state.getRetryCount() == 2));
verifyNoMoreInteractions(retryListener);
}
@Test
void supplierWithExhaustedPolicy() {
AtomicInteger invocationCount = new AtomicInteger();
Supplier<String> retryable = () -> {
throw new CustomException("Boom " + invocationCount.incrementAndGet());
};
assertThat(invocationCount).hasValue(0);
assertThatExceptionOfType(CustomException.class)
.isThrownBy(() -> retryTemplate.invoke(retryable))
.withMessage("Boom 4")
.satisfies(throwable -> {
var counter = new AtomicInteger(1);
repeat(3, () -> {
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
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())));
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
eq(new CustomException("Boom " + counter.incrementAndGet())));
});
inOrder.verify(retryListener).onRetryableExecution(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
argThat(state -> !state.isSuccessful() && state.getRetryCount() == 3));
inOrder.verify(retryListener).onRetryPolicyExhaustion(eq(retryPolicy),
argThat(r -> r.getName().equals(retryable.getClass().getName())),
argThat(t -> t.getMessage().equals("Retry policy for operation '" +
retryable.getClass().getName() + "' exhausted; aborting execution")));
});
// 4 = 1 initial invocation + 3 retry attempts
assertThat(invocationCount).hasValue(4);
verifyNoMoreInteractions(retryListener);
}
@Nested
class TimeoutTests {
@Test
void retryWithImmediateSuccessAndTimeoutExceeded() throws Exception {
void retryableWithImmediateSuccessAndTimeoutExceeded() throws Exception {
RetryPolicy retryPolicy = RetryPolicy.builder().timeout(Duration.ofMillis(10)).build();
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
retryTemplate.setRetryListener(retryListener);
@@ -464,7 +557,7 @@ class RetryTemplateTests {
}
@Test
void retryWithInitialFailureAndZeroRetriesRetryPolicyAndTimeoutExceeded() {
void retryableWithInitialFailureAndZeroRetriesRetryPolicyAndTimeoutExceeded() {
RetryPolicy retryPolicy = RetryPolicy.builder()
.timeout(Duration.ofMillis(10))
.predicate(throwable -> false) // Zero retries
@@ -492,7 +585,7 @@ class RetryTemplateTests {
}
@Test
void retryWithTimeoutExceededAfterInitialFailure() {
void retryableWithTimeoutExceededAfterInitialFailure() {
RetryPolicy retryPolicy = RetryPolicy.builder()
.timeout(Duration.ofMillis(10))
.delay(Duration.ZERO)
@@ -521,7 +614,7 @@ class RetryTemplateTests {
}
@Test
void retryWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry() {
void retryableWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry() {
RetryPolicy retryPolicy = RetryPolicy.builder()
.timeout(Duration.ofMillis(20))
.delay(Duration.ofMillis(100)) // Delay > Timeout
@@ -552,7 +645,7 @@ class RetryTemplateTests {
}
@Test
void retryWithTimeoutExceededAfterFirstRetry() {
void retryableWithTimeoutExceededAfterFirstRetry() {
RetryPolicy retryPolicy = RetryPolicy.builder()
.timeout(Duration.ofMillis(20))
.delay(Duration.ZERO)
@@ -589,7 +682,7 @@ class RetryTemplateTests {
}
@Test
void retryWithTimeoutExceededAfterSecondRetry() {
void retryableWithTimeoutExceededAfterSecondRetry() {
RetryPolicy retryPolicy = RetryPolicy.builder()
.timeout(Duration.ofMillis(20))
.delay(Duration.ZERO)