mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-10-03 05:19:07 +00:00
Add invoke(Runnable) as variant of invoke(Supplier)
See gh-36052
This commit is contained in:
@@ -66,4 +66,14 @@ public interface RetryOperations {
|
||||
*/
|
||||
<R extends @Nullable Object> R invoke(Supplier<R> retryable);
|
||||
|
||||
/**
|
||||
* Invoke the given {@link Runnable} according to the {@link RetryPolicy},
|
||||
* returning successfully or throwing the last {@code Runnable} exception
|
||||
* to the caller in case of retry policy exhaustion.
|
||||
* @param retryable the {@code Runnable} to invoke and retry if needed
|
||||
* @throws RuntimeException if thrown by the {@code Runnable}
|
||||
* @since 7.0.3
|
||||
*/
|
||||
void invoke(Runnable retryable);
|
||||
|
||||
}
|
||||
|
||||
@@ -254,6 +254,33 @@ public class RetryTemplate implements RetryOperations {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(Runnable retryable) {
|
||||
try {
|
||||
execute(new Retryable<>() {
|
||||
@Override
|
||||
public Void execute() {
|
||||
retryable.run();
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
public String getName() {
|
||||
return retryable.getClass().getName();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (RetryException retryException) {
|
||||
Throwable ex = retryException.getCause();
|
||||
if (ex instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (ex instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new UndeclaredThrowableException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class MutableRetryState implements RetryState {
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ 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.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
@@ -529,6 +530,95 @@ class RetryTemplateTests {
|
||||
verifyNoMoreInteractions(retryListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runnableWithImmediateSuccess() {
|
||||
AtomicInteger invocationCount = new AtomicInteger();
|
||||
Runnable retryable = invocationCount::incrementAndGet;
|
||||
|
||||
assertThat(invocationCount).hasValue(0);
|
||||
assertThatNoException().isThrownBy(() -> retryTemplate.invoke(retryable));
|
||||
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 runnableWithSuccessAfterInitialFailures() {
|
||||
AtomicInteger invocationCount = new AtomicInteger();
|
||||
Runnable retryable = () -> {
|
||||
if (invocationCount.incrementAndGet() <= 2) {
|
||||
throw new CustomException("Boom " + invocationCount.get());
|
||||
}
|
||||
};
|
||||
|
||||
assertThat(invocationCount).hasValue(0);
|
||||
assertThatNoException().isThrownBy(() -> retryTemplate.invoke(retryable));
|
||||
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(null));
|
||||
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 runnableWithExhaustedPolicy() {
|
||||
AtomicInteger invocationCount = new AtomicInteger();
|
||||
Runnable 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 {
|
||||
|
||||
Reference in New Issue
Block a user