mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-25 08:19:01 +00:00
Support timeouts in @Retryable and RetryPolicy
Specifically, this commit introduces: - timeout and timeoutString attributes in @Retryable - a default getTimeout() method in RetryPolicy - a timeout() method in RetryPolicy.Builder - an onRetryPolicyTimeout() callback in RetryListener - support for checking exceeded timeouts in RetryTemplate (also used for imperative method invocations with @Retryable) - support for checking exceeded timeouts in reactive pipelines with @Retryable Closes gh-35963
This commit is contained in:
+134
@@ -21,9 +21,11 @@ import java.nio.charset.MalformedInputException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.assertj.core.api.ThrowingConsumer;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -316,6 +318,83 @@ class ReactiveRetryInterceptorTests {
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class TimeoutTests {
|
||||
|
||||
private final AnnotatedMethodBean proxy = getProxiedAnnotatedMethodBean();
|
||||
private final AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
@Test
|
||||
void timeoutNotExceededAfterInitialSuccess() {
|
||||
String result = proxy.retryOperationWithTimeoutNotExceededAfterInitialSuccess().block();
|
||||
assertThat(result).isEqualTo("success");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutNotExceededAndRetriesExhausted() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutNotExceededAndRetriesExhausted().block())
|
||||
.satisfies(isRetryExhaustedException())
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("4");
|
||||
// 1 initial attempt + 3 retries
|
||||
assertThat(target.counter).hasValue(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterInitialFailure() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutExceededAfterInitialFailure().block())
|
||||
.satisfies(isReactiveException())
|
||||
.havingCause()
|
||||
.isInstanceOf(TimeoutException.class)
|
||||
.withMessageContaining("within 5ms");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterFirstDelayButBeforeFirstRetry() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry().block())
|
||||
.satisfies(isReactiveException())
|
||||
.havingCause()
|
||||
.isInstanceOf(TimeoutException.class)
|
||||
.withMessageContaining("within 5ms");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterFirstRetry() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutExceededAfterFirstRetry().block())
|
||||
.satisfies(isReactiveException())
|
||||
.havingCause()
|
||||
.isInstanceOf(TimeoutException.class)
|
||||
.withMessageContaining("within 5ms");
|
||||
// 1 initial attempt + 1 retry
|
||||
assertThat(target.counter).hasValue(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterSecondRetry() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutExceededAfterSecondRetry().block())
|
||||
.satisfies(isReactiveException())
|
||||
.havingCause()
|
||||
.isInstanceOf(TimeoutException.class)
|
||||
.withMessageContaining("within 5ms");
|
||||
// 1 initial attempt + 2 retries
|
||||
assertThat(target.counter).hasValue(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static ThrowingConsumer<? super Throwable> isReactiveException() {
|
||||
return ex -> assertThat(ex.getClass().getName()).isEqualTo("reactor.core.Exceptions$ReactiveException");
|
||||
}
|
||||
@@ -368,6 +447,61 @@ class ReactiveRetryInterceptorTests {
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public Mono<String> retryOperationWithTimeoutNotExceededAfterInitialSuccess() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
return "success";
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public Mono<Object> retryOperationWithTimeoutNotExceededAndRetriesExhausted() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 5, delay = 10)
|
||||
public Mono<Object> retryOperationWithTimeoutExceededAfterInitialFailure() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
Thread.sleep(10);
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 5, delay = 10)
|
||||
public Mono<Object> retryOperationWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 5, delay = 0)
|
||||
public Mono<Object> retryOperationWithTimeoutExceededAfterFirstRetry() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
if (counter.get() == 2) {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 5, delay = 0)
|
||||
public Mono<Object> retryOperationWithTimeoutExceededAfterSecondRetry() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
if (counter.get() == 3) {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+107
@@ -27,6 +27,7 @@ import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.config.AopConfigUtils;
|
||||
@@ -312,6 +313,69 @@ class RetryInterceptorTests {
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class TimeoutTests {
|
||||
|
||||
private final DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedMethodBean.class);
|
||||
private final AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class);
|
||||
private final AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
@Test
|
||||
void timeoutNotExceededAfterInitialSuccess() {
|
||||
String result = proxy.retryOperationWithTimeoutNotExceededAfterInitialSuccess();
|
||||
assertThat(result).isEqualTo("success");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutNotExceededAndRetriesExhausted() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutNotExceededAndRetriesExhausted)
|
||||
.withMessage("4");
|
||||
// 1 initial attempt + 3 retries
|
||||
assertThat(target.counter).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterInitialFailure() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutExceededAfterInitialFailure)
|
||||
.withMessage("1");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterFirstDelayButBeforeFirstRetry() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry)
|
||||
.withMessage("1");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterFirstRetry() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutExceededAfterFirstRetry)
|
||||
.withMessage("2");
|
||||
// 1 initial attempt + 1 retry
|
||||
assertThat(target.counter).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterSecondRetry() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutExceededAfterSecondRetry)
|
||||
.withMessage("3");
|
||||
// 1 initial attempt + 2 retries
|
||||
assertThat(target.counter).isEqualTo(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static DefaultListableBeanFactory createBeanFactoryFor(Class<?> beanClass) {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(beanClass));
|
||||
@@ -349,6 +413,49 @@ class RetryInterceptorTests {
|
||||
counter++;
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public String retryOperationWithTimeoutNotExceededAfterInitialSuccess() {
|
||||
counter++;
|
||||
return "success";
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public void retryOperationWithTimeoutNotExceededAndRetriesExhausted() throws Exception {
|
||||
counter++;
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 5, delay = 10)
|
||||
public void retryOperationWithTimeoutExceededAfterInitialFailure() throws Exception {
|
||||
counter++;
|
||||
Thread.sleep(10);
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 5, delay = 10)
|
||||
public void retryOperationWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry() throws IOException {
|
||||
counter++;
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 5, delay = 0)
|
||||
public void retryOperationWithTimeoutExceededAfterFirstRetry() throws Exception {
|
||||
counter++;
|
||||
if (counter == 2) {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 5, delay = 0)
|
||||
public void retryOperationWithTimeoutExceededAfterSecondRetry() throws Exception {
|
||||
counter++;
|
||||
if (counter == 3) {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user