Browse Source

Add invoke(Runnable) as variant of invoke(Supplier)

See gh-36052
pull/36071/head
Juergen Hoeller 1 month ago
parent
commit
85c6fb0fd0
  1. 10
      spring-core/src/main/java/org/springframework/core/retry/RetryOperations.java
  2. 27
      spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java
  3. 90
      spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java

10
spring-core/src/main/java/org/springframework/core/retry/RetryOperations.java

@ -66,4 +66,14 @@ public interface RetryOperations { @@ -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);
}

27
spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java

@ -254,6 +254,33 @@ public class RetryTemplate implements RetryOperations { @@ -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 {

90
spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java

@ -39,6 +39,7 @@ import org.springframework.util.backoff.BackOff; @@ -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 { @@ -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 {

Loading…
Cancel
Save