Skip to content

Commit 132836f

Browse files
committed
Add missing "includes mismatch" test in ReactiveRetryInterceptorTests
This commit also overhauls ReactiveRetryInterceptorTests to make the tests more robust and simultaneously easier to comprehend.
1 parent 27072cc commit 132836f

File tree

1 file changed

+149
-43
lines changed

1 file changed

+149
-43
lines changed

spring-context/src/test/java/org/springframework/resilience/ReactiveRetryInterceptorTests.java

Lines changed: 149 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
import java.io.IOException;
2020
import java.lang.reflect.Method;
2121
import java.nio.file.AccessDeniedException;
22+
import java.nio.file.FileSystemException;
2223
import java.time.Duration;
2324
import java.util.concurrent.atomic.AtomicInteger;
2425

2526
import org.assertj.core.api.ThrowingConsumer;
2627
import org.junit.jupiter.api.Test;
28+
import reactor.core.Exceptions;
2729
import reactor.core.publisher.Flux;
2830
import reactor.core.publisher.Mono;
2931

@@ -38,11 +40,13 @@
3840
import org.springframework.resilience.retry.SimpleRetryInterceptor;
3941

4042
import static org.assertj.core.api.Assertions.assertThat;
43+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
4144
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
4245
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
4346

4447
/**
4548
* @author Juergen Hoeller
49+
* @author Sam Brannen
4650
* @since 7.0
4751
*/
4852
class ReactiveRetryInterceptorTests {
@@ -56,9 +60,12 @@ void withSimpleInterceptor() {
5660
new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10))));
5761
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy();
5862

59-
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block())
63+
assertThatIllegalStateException()
64+
.isThrownBy(() -> proxy.retryOperation().block())
6065
.satisfies(isRetryExhaustedException())
61-
.withCauseInstanceOf(IOException.class).havingCause().withMessage("6");
66+
.havingCause()
67+
.isInstanceOf(IOException.class)
68+
.withMessage("6");
6269
assertThat(target.counter.get()).isEqualTo(6);
6370
}
6471

@@ -72,34 +79,94 @@ void withPostProcessorForMethod() {
7279
AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class);
7380
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
7481

75-
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block())
82+
assertThatIllegalStateException()
83+
.isThrownBy(() -> proxy.retryOperation().block())
7684
.satisfies(isRetryExhaustedException())
77-
.withCauseInstanceOf(IOException.class).havingCause().withMessage("6");
85+
.havingCause()
86+
.isInstanceOf(IOException.class)
87+
.withMessage("6");
7888
assertThat(target.counter.get()).isEqualTo(6);
7989
}
8090

8191
@Test
82-
void withPostProcessorForClass() {
83-
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
84-
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class));
85-
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
86-
bpp.setBeanFactory(bf);
87-
bf.addBeanPostProcessor(bpp);
88-
AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class);
92+
void withPostProcessorForClassWithExactIncludesMatch() {
93+
AnnotatedClassBean proxy = getProxiedAnnotatedClassBean();
8994
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
9095

91-
assertThatRuntimeException().isThrownBy(() -> proxy.retryOperation().block())
96+
// Exact includes match: IOException
97+
assertThatRuntimeException()
98+
.isThrownBy(() -> proxy.ioOperation().block())
99+
// Does NOT throw a RetryExhaustedException, because IOException3Predicate
100+
// returns false once the exception's message is "3".
92101
.satisfies(isReactiveException())
93-
.withCauseInstanceOf(IOException.class).havingCause().withMessage("3");
102+
.havingCause()
103+
.isInstanceOf(IOException.class)
104+
.withMessage("3");
105+
// 1 initial attempt + 2 retries
94106
assertThat(target.counter.get()).isEqualTo(3);
95-
assertThatRuntimeException().isThrownBy(() -> proxy.otherOperation().block())
96-
.satisfies(isReactiveException())
97-
.withCauseInstanceOf(IOException.class);
107+
}
108+
109+
@Test
110+
void withPostProcessorForClassWithSubtypeIncludesMatch() {
111+
AnnotatedClassBean proxy = getProxiedAnnotatedClassBean();
112+
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
113+
114+
// Subtype includes match: FileSystemException
115+
assertThatRuntimeException()
116+
.isThrownBy(() -> proxy.fileSystemOperation().block())
117+
.satisfies(isRetryExhaustedException())
118+
.withCauseInstanceOf(FileSystemException.class);
119+
// 1 initial attempt + 3 retries
98120
assertThat(target.counter.get()).isEqualTo(4);
99-
assertThatIllegalStateException().isThrownBy(() -> proxy.overrideOperation().blockFirst())
121+
}
122+
123+
@Test
124+
void withPostProcessorForClassWithExcludesMatch() {
125+
AnnotatedClassBean proxy = getProxiedAnnotatedClassBean();
126+
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
127+
128+
// Exact excludes match: AccessDeniedException
129+
assertThatRuntimeException()
130+
.isThrownBy(() -> proxy.accessOperation().block())
131+
// Does NOT throw a RetryExhaustedException, because no retry is
132+
// performed for an AccessDeniedException.
133+
.satisfies(isReactiveException())
134+
.withCauseInstanceOf(AccessDeniedException.class);
135+
// 1 initial attempt + 0 retries
136+
assertThat(target.counter.get()).isEqualTo(1);
137+
}
138+
139+
@Test
140+
void withPostProcessorForClassWithIncludesMismatch() {
141+
AnnotatedClassBean proxy = getProxiedAnnotatedClassBean();
142+
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
143+
144+
// No match: ArithmeticException
145+
//
146+
// Does NOT throw a RetryExhaustedException because no retry is performed
147+
// for an ArithmeticException, since it is not an IOException.
148+
// Does NOT throw a ReactiveException because ArithmeticException is a
149+
// RuntimeException, which reactor.core.Exceptions.propagate(Throwable)
150+
// does not wrap.
151+
assertThatExceptionOfType(ArithmeticException.class)
152+
.isThrownBy(() -> proxy.arithmeticOperation().block())
153+
.withMessage("1");
154+
// 1 initial attempt + 0 retries
155+
assertThat(target.counter.get()).isEqualTo(1);
156+
}
157+
158+
@Test
159+
void withPostProcessorForClassWithMethodLevelOverride() {
160+
AnnotatedClassBean proxy = getProxiedAnnotatedClassBean();
161+
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
162+
163+
// Overridden, local @Retryable declaration
164+
assertThatIllegalStateException()
165+
.isThrownBy(() -> proxy.overrideOperation().blockFirst())
100166
.satisfies(isRetryExhaustedException())
101167
.withCauseInstanceOf(IOException.class);
102-
assertThat(target.counter.get()).isEqualTo(6);
168+
// 1 initial attempt + 1 retry
169+
assertThat(target.counter.get()).isEqualTo(2);
103170
}
104171

105172
@Test
@@ -113,9 +180,12 @@ void adaptReactiveResultWithMinimalRetrySpec() {
113180
MinimalRetryBean proxy = (MinimalRetryBean) pf.getProxy();
114181

115182
// Should execute only 2 times, because maxAttempts=1 means 1 call + 1 retry
116-
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block())
183+
assertThatIllegalStateException()
184+
.isThrownBy(() -> proxy.retryOperation().block())
117185
.satisfies(isRetryExhaustedException())
118-
.withCauseInstanceOf(IOException.class).havingCause().withMessage("2");
186+
.havingCause()
187+
.isInstanceOf(IOException.class)
188+
.withMessage("2");
119189
assertThat(target.counter.get()).isEqualTo(2);
120190
}
121191

@@ -129,9 +199,12 @@ void adaptReactiveResultWithZeroDelayAndJitter() {
129199
new MethodRetrySpec((m, t) -> true, 3, Duration.ZERO, Duration.ofMillis(10), 2.0, Duration.ofMillis(100))));
130200
ZeroDelayJitterBean proxy = (ZeroDelayJitterBean) pf.getProxy();
131201

132-
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block())
202+
assertThatIllegalStateException()
203+
.isThrownBy(() -> proxy.retryOperation().block())
133204
.satisfies(isRetryExhaustedException())
134-
.withCauseInstanceOf(IOException.class).havingCause().withMessage("4");
205+
.havingCause()
206+
.isInstanceOf(IOException.class)
207+
.withMessage("4");
135208
assertThat(target.counter.get()).isEqualTo(4);
136209
}
137210

@@ -145,9 +218,12 @@ void adaptReactiveResultWithJitterGreaterThanDelay() {
145218
new MethodRetrySpec((m, t) -> true, 3, Duration.ofMillis(5), Duration.ofMillis(20), 1.5, Duration.ofMillis(50))));
146219
JitterGreaterThanDelayBean proxy = (JitterGreaterThanDelayBean) pf.getProxy();
147220

148-
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block())
149-
.satisfies(ex -> assertThat(ex.getClass().getSimpleName()).isEqualTo("RetryExhaustedException"))
150-
.withCauseInstanceOf(IOException.class).havingCause().withMessage("4");
221+
assertThatIllegalStateException()
222+
.isThrownBy(() -> proxy.retryOperation().block())
223+
.satisfies(isRetryExhaustedException())
224+
.havingCause()
225+
.isInstanceOf(IOException.class)
226+
.withMessage("4");
151227
assertThat(target.counter.get()).isEqualTo(4);
152228
}
153229

@@ -161,9 +237,12 @@ void adaptReactiveResultWithFluxMultiValue() {
161237
new MethodRetrySpec((m, t) -> true, 3, Duration.ofMillis(10), Duration.ofMillis(5), 2.0, Duration.ofMillis(100))));
162238
FluxMultiValueBean proxy = (FluxMultiValueBean) pf.getProxy();
163239

164-
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().blockFirst())
240+
assertThatIllegalStateException()
241+
.isThrownBy(() -> proxy.retryOperation().blockFirst())
165242
.satisfies(isRetryExhaustedException())
166-
.withCauseInstanceOf(IOException.class).havingCause().withMessage("4");
243+
.havingCause()
244+
.isInstanceOf(IOException.class)
245+
.withMessage("4");
167246
assertThat(target.counter.get()).isEqualTo(4);
168247
}
169248

@@ -184,28 +263,41 @@ void adaptReactiveResultWithSuccessfulOperation() {
184263
}
185264

186265
@Test
187-
void adaptReactiveResultWithImmediateFailure() {
188-
// Test immediate failure case
189-
ImmediateFailureBean target = new ImmediateFailureBean();
266+
void adaptReactiveResultWithAlwaysFailingOperation() {
267+
// Test "always fails" case, ensuring retry mechanism stops after maxAttempts (3)
268+
AlwaysFailsBean target = new AlwaysFailsBean();
190269
ProxyFactory pf = new ProxyFactory();
191270
pf.setTarget(target);
192271
pf.addAdvice(new SimpleRetryInterceptor(
193272
new MethodRetrySpec((m, t) -> true, 3, Duration.ofMillis(10), Duration.ofMillis(5), 1.5, Duration.ofMillis(50))));
194-
ImmediateFailureBean proxy = (ImmediateFailureBean) pf.getProxy();
273+
AlwaysFailsBean proxy = (AlwaysFailsBean) pf.getProxy();
195274

196-
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block())
275+
assertThatIllegalStateException()
276+
.isThrownBy(() -> proxy.retryOperation().block())
197277
.satisfies(isRetryExhaustedException())
198-
.withCauseInstanceOf(RuntimeException.class).havingCause().withMessage("immediate failure");
278+
.havingCause()
279+
.isInstanceOf(NumberFormatException.class)
280+
.withMessage("always fails");
281+
// 1 initial attempt + 3 retries
199282
assertThat(target.counter.get()).isEqualTo(4);
200283
}
201284

202285

203286
private static ThrowingConsumer<? super Throwable> isReactiveException() {
204-
return ex -> assertThat(ex.getClass().getSimpleName()).isEqualTo("ReactiveException");
287+
return ex -> assertThat(ex.getClass().getName()).isEqualTo("reactor.core.Exceptions$ReactiveException");
205288
}
206289

207290
private static ThrowingConsumer<? super Throwable> isRetryExhaustedException() {
208-
return ex -> assertThat(ex.getClass().getSimpleName()).isEqualTo("RetryExhaustedException");
291+
return ex -> assertThat(ex).matches(Exceptions::isRetryExhausted, "is RetryExhaustedException");
292+
}
293+
294+
private static AnnotatedClassBean getProxiedAnnotatedClassBean() {
295+
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
296+
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class));
297+
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
298+
bpp.setBeanFactory(bf);
299+
bf.addBeanPostProcessor(bpp);
300+
return bf.getBean(AnnotatedClassBean.class);
209301
}
210302

211303

@@ -238,26 +330,40 @@ public Mono<Object> retryOperation() {
238330

239331
@Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40,
240332
includes = IOException.class, excludes = AccessDeniedException.class,
241-
predicate = CustomPredicate.class)
333+
predicate = IOException3Predicate.class)
242334
static class AnnotatedClassBean {
243335

244336
AtomicInteger counter = new AtomicInteger();
245337

246-
public Mono<Object> retryOperation() {
338+
public Mono<Object> ioOperation() {
247339
return Mono.fromCallable(() -> {
248340
counter.incrementAndGet();
249341
throw new IOException(counter.toString());
250342
});
251343
}
252344

253-
public Mono<Object> otherOperation() {
345+
public Mono<Object> fileSystemOperation() {
346+
return Mono.fromCallable(() -> {
347+
counter.incrementAndGet();
348+
throw new FileSystemException(counter.toString());
349+
});
350+
}
351+
352+
public Mono<Object> accessOperation() {
254353
return Mono.fromCallable(() -> {
255354
counter.incrementAndGet();
256355
throw new AccessDeniedException(counter.toString());
257356
});
258357
}
259358

260-
@Retryable(value = IOException.class, maxAttempts = 1, delay = 10)
359+
public Mono<Object> arithmeticOperation() {
360+
return Mono.fromCallable(() -> {
361+
counter.incrementAndGet();
362+
throw new ArithmeticException(counter.toString());
363+
});
364+
}
365+
366+
@Retryable(includes = IOException.class, maxAttempts = 1, delay = 10)
261367
public Flux<Object> overrideOperation() {
262368
return Flux.from(Mono.fromCallable(() -> {
263369
counter.incrementAndGet();
@@ -267,11 +373,11 @@ public Flux<Object> overrideOperation() {
267373
}
268374

269375

270-
private static class CustomPredicate implements MethodRetryPredicate {
376+
private static class IOException3Predicate implements MethodRetryPredicate {
271377

272378
@Override
273379
public boolean shouldRetry(Method method, Throwable throwable) {
274-
return !"3".equals(throwable.getMessage());
380+
return !(throwable.getClass() == IOException.class && "3".equals(throwable.getMessage()));
275381
}
276382
}
277383

@@ -343,14 +449,14 @@ public Mono<String> retryOperation() {
343449
}
344450

345451

346-
static class ImmediateFailureBean {
452+
static class AlwaysFailsBean {
347453

348454
AtomicInteger counter = new AtomicInteger();
349455

350456
public Mono<Object> retryOperation() {
351457
return Mono.fromCallable(() -> {
352458
counter.incrementAndGet();
353-
throw new RuntimeException("immediate failure");
459+
throw new NumberFormatException("always fails");
354460
});
355461
}
356462
}

0 commit comments

Comments
 (0)