mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-10-07 23:49:08 +00:00
Introduce MethodRetryEvent for @Retryable execution
Closes gh-35382
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.resilience;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.resilience.retry.MethodRetryEvent;
|
||||
|
||||
/**
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0.3
|
||||
*/
|
||||
class MethodRetryEventListener implements ApplicationListener<MethodRetryEvent> {
|
||||
|
||||
public final List<MethodRetryEvent> events = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(MethodRetryEvent event) {
|
||||
this.events.add(event);
|
||||
}
|
||||
|
||||
}
|
||||
+84
-6
@@ -17,6 +17,7 @@
|
||||
package org.springframework.resilience;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.MalformedInputException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.FileSystemException;
|
||||
@@ -33,8 +34,11 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.resilience.annotation.EnableResilientMethods;
|
||||
import org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor;
|
||||
import org.springframework.resilience.annotation.Retryable;
|
||||
import org.springframework.resilience.retry.MethodRetrySpec;
|
||||
@@ -43,6 +47,7 @@ import org.springframework.resilience.retry.SimpleRetryInterceptor;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
|
||||
|
||||
/**
|
||||
@@ -184,6 +189,52 @@ class ReactiveRetryInterceptorTests {
|
||||
assertThat(target.counter).hasValue(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withMethodRetryEventListener() throws Exception {
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
ctx.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class));
|
||||
ctx.registerBeanDefinition("config", new RootBeanDefinition(EnablingConfig.class));
|
||||
MethodRetryEventListener listener = new MethodRetryEventListener();
|
||||
ctx.addApplicationListener(listener);
|
||||
ctx.refresh();
|
||||
AnnotatedMethodBean proxy = ctx.getBean(AnnotatedMethodBean.class);
|
||||
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
Method method1 = AnnotatedMethodBean.class.getMethod("retryOperation");
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> proxy.retryOperation().block())
|
||||
.satisfies(isRetryExhaustedException());
|
||||
assertThat(target.counter).hasValue(6);
|
||||
assertThat(listener.events).hasSize(7);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
String msg = Integer.toString(i + 1);
|
||||
assertThat(listener.events.get(i))
|
||||
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method1))
|
||||
.satisfies(event -> assertThat(event.getFailure()).hasMessage(msg).isInstanceOf(IOException.class))
|
||||
.satisfies(event -> assertThat(event.isRetryAborted()).isFalse());
|
||||
}
|
||||
assertThat(listener.events.get(6))
|
||||
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method1))
|
||||
.satisfies(event -> assertThat(event.getFailure()).satisfies(isRetryExhaustedException()))
|
||||
.satisfies(event -> assertThat(event.isRetryAborted()).isTrue());
|
||||
|
||||
listener.events.clear();
|
||||
target.counter.set(0);
|
||||
assertThatNoException().isThrownBy(() -> proxy.retryOperationWithInitialSuccess().block());
|
||||
assertThat(target.counter).hasValue(1);
|
||||
assertThat(listener.events).isEmpty();
|
||||
|
||||
target.counter.set(0);
|
||||
Method method2 = AnnotatedMethodBean.class.getMethod("retryOperationWithSuccessAfterInitialFailure");
|
||||
assertThatNoException().isThrownBy(() -> proxy.retryOperationWithSuccessAfterInitialFailure().block());
|
||||
assertThat(target.counter).hasValue(2);
|
||||
assertThat(listener.events).hasSize(1);
|
||||
assertThat(listener.events.get(0))
|
||||
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method2))
|
||||
.satisfies(event -> assertThat(event.getFailure()).hasMessage("1").isInstanceOf(IOException.class))
|
||||
.satisfies(event -> assertThat(event.isRetryAborted()).isFalse());
|
||||
}
|
||||
|
||||
@Test
|
||||
void adaptReactiveResultWithMinimalRetrySpec() {
|
||||
// Test minimal retry configuration: maxRetries=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
|
||||
@@ -391,7 +442,6 @@ class ReactiveRetryInterceptorTests {
|
||||
// 1 initial attempt + 2 retries
|
||||
assertThat(target.counter).hasValue(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -404,21 +454,29 @@ class ReactiveRetryInterceptorTests {
|
||||
}
|
||||
|
||||
private static AnnotatedMethodBean getProxiedAnnotatedMethodBean() {
|
||||
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedMethodBean.class);
|
||||
BeanFactory bf = createBeanFactoryFor(AnnotatedMethodBean.class);
|
||||
return bf.getBean(AnnotatedMethodBean.class);
|
||||
}
|
||||
|
||||
private static AnnotatedClassBean getProxiedAnnotatedClassBean() {
|
||||
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedClassBean.class);
|
||||
BeanFactory bf = createBeanFactoryFor(AnnotatedClassBean.class);
|
||||
return bf.getBean(AnnotatedClassBean.class);
|
||||
}
|
||||
|
||||
private static DefaultListableBeanFactory createBeanFactoryFor(Class<?> beanClass) {
|
||||
private static BeanFactory createBeanFactoryFor(Class<?> beanClass) {
|
||||
/*
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(beanClass));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
*/
|
||||
GenericApplicationContext bf = new GenericApplicationContext();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(beanClass));
|
||||
bf.registerBeanDefinition("processor", new RootBeanDefinition(RetryAnnotationBeanPostProcessor.class));
|
||||
bf.registerBeanDefinition("listener", new RootBeanDefinition(MethodRetryEventListener.class));
|
||||
bf.refresh();
|
||||
|
||||
return bf;
|
||||
}
|
||||
|
||||
@@ -448,6 +506,24 @@ class ReactiveRetryInterceptorTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(maxRetries = 5, delay = 10)
|
||||
public Mono<String> retryOperationWithInitialSuccess() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
return "success";
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(maxRetries = 5, delay = 10)
|
||||
public Mono<String> retryOperationWithSuccessAfterInitialFailure() {
|
||||
return Mono.fromCallable(() -> {
|
||||
if (counter.incrementAndGet() == 1) {
|
||||
throw new IOException(counter.toString());
|
||||
}
|
||||
return "success";
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public Mono<String> retryOperationWithTimeoutNotExceededAfterInitialSuccess() {
|
||||
return Mono.fromCallable(() -> {
|
||||
@@ -560,7 +636,9 @@ class ReactiveRetryInterceptorTests {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@EnableResilientMethods
|
||||
static class EnablingConfig {
|
||||
}
|
||||
|
||||
|
||||
// Bean classes for boundary testing
|
||||
|
||||
+61
-1
@@ -18,6 +18,7 @@ package org.springframework.resilience;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.MalformedInputException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.time.Duration;
|
||||
@@ -41,6 +42,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.retry.RetryException;
|
||||
import org.springframework.resilience.annotation.ConcurrencyLimit;
|
||||
import org.springframework.resilience.annotation.EnableResilientMethods;
|
||||
import org.springframework.resilience.annotation.RetryAnnotationBeanPostProcessor;
|
||||
@@ -53,6 +55,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIOException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
|
||||
|
||||
/**
|
||||
@@ -312,6 +315,50 @@ class RetryInterceptorTests {
|
||||
assertThat(target.counter).hasValue(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withMethodRetryEventListener() throws Exception {
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
ctx.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class));
|
||||
ctx.registerBeanDefinition("config", new RootBeanDefinition(EnablingConfig.class));
|
||||
MethodRetryEventListener listener = new MethodRetryEventListener();
|
||||
ctx.addApplicationListener(listener);
|
||||
ctx.refresh();
|
||||
AnnotatedMethodBean proxy = ctx.getBean(AnnotatedMethodBean.class);
|
||||
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
Method method1 = AnnotatedMethodBean.class.getMethod("retryOperation");
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
assertThat(listener.events).hasSize(7);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
String msg = Integer.toString(i + 1);
|
||||
assertThat(listener.events.get(i))
|
||||
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method1))
|
||||
.satisfies(event -> assertThat(event.getFailure()).hasMessage(msg).isInstanceOf(IOException.class))
|
||||
.satisfies(event -> assertThat(event.isRetryAborted()).isFalse());
|
||||
}
|
||||
assertThat(listener.events.get(6))
|
||||
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method1))
|
||||
.satisfies(event -> assertThat(event.getFailure()).isInstanceOf(RetryException.class))
|
||||
.satisfies(event -> assertThat(event.isRetryAborted()).isTrue());
|
||||
|
||||
listener.events.clear();
|
||||
target.counter = 0;
|
||||
assertThatNoException().isThrownBy(proxy::retryOperationWithInitialSuccess);
|
||||
assertThat(target.counter).isEqualTo(1);
|
||||
assertThat(listener.events).isEmpty();
|
||||
|
||||
target.counter = 0;
|
||||
Method method2 = AnnotatedMethodBean.class.getMethod("retryOperationWithSuccessAfterInitialFailure");
|
||||
assertThatNoException().isThrownBy(proxy::retryOperationWithSuccessAfterInitialFailure);
|
||||
assertThat(target.counter).isEqualTo(2);
|
||||
assertThat(listener.events).hasSize(1);
|
||||
assertThat(listener.events.get(0))
|
||||
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method2))
|
||||
.satisfies(event -> assertThat(event.getFailure()).hasMessage("1").isInstanceOf(IOException.class))
|
||||
.satisfies(event -> assertThat(event.isRetryAborted()).isFalse());
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class TimeoutTests {
|
||||
@@ -372,7 +419,6 @@ class RetryInterceptorTests {
|
||||
// 1 initial attempt + 2 retries
|
||||
assertThat(target.counter).isEqualTo(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -414,6 +460,20 @@ class RetryInterceptorTests {
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(maxRetries = 5, delay = 10)
|
||||
public String retryOperationWithInitialSuccess() {
|
||||
counter++;
|
||||
return "success";
|
||||
}
|
||||
|
||||
@Retryable(maxRetries = 5, delay = 10)
|
||||
public String retryOperationWithSuccessAfterInitialFailure() throws IOException{
|
||||
if (++counter == 1) {
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
return "success";
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public String retryOperationWithTimeoutNotExceededAfterInitialSuccess() {
|
||||
counter++;
|
||||
|
||||
Reference in New Issue
Block a user