19
19
import java .io .IOException ;
20
20
import java .lang .reflect .Method ;
21
21
import java .nio .file .AccessDeniedException ;
22
+ import java .nio .file .FileSystemException ;
22
23
import java .time .Duration ;
23
24
import java .util .concurrent .atomic .AtomicInteger ;
24
25
25
26
import org .assertj .core .api .ThrowingConsumer ;
26
27
import org .junit .jupiter .api .Test ;
28
+ import reactor .core .Exceptions ;
27
29
import reactor .core .publisher .Flux ;
28
30
import reactor .core .publisher .Mono ;
29
31
38
40
import org .springframework .resilience .retry .SimpleRetryInterceptor ;
39
41
40
42
import static org .assertj .core .api .Assertions .assertThat ;
43
+ import static org .assertj .core .api .Assertions .assertThatExceptionOfType ;
41
44
import static org .assertj .core .api .Assertions .assertThatIllegalStateException ;
42
45
import static org .assertj .core .api .Assertions .assertThatRuntimeException ;
43
46
44
47
/**
45
48
* @author Juergen Hoeller
49
+ * @author Sam Brannen
46
50
* @since 7.0
47
51
*/
48
52
class ReactiveRetryInterceptorTests {
@@ -56,9 +60,12 @@ void withSimpleInterceptor() {
56
60
new MethodRetrySpec ((m , t ) -> true , 5 , Duration .ofMillis (10 ))));
57
61
NonAnnotatedBean proxy = (NonAnnotatedBean ) pf .getProxy ();
58
62
59
- assertThatIllegalStateException ().isThrownBy (() -> proxy .retryOperation ().block ())
63
+ assertThatIllegalStateException ()
64
+ .isThrownBy (() -> proxy .retryOperation ().block ())
60
65
.satisfies (isRetryExhaustedException ())
61
- .withCauseInstanceOf (IOException .class ).havingCause ().withMessage ("6" );
66
+ .havingCause ()
67
+ .isInstanceOf (IOException .class )
68
+ .withMessage ("6" );
62
69
assertThat (target .counter .get ()).isEqualTo (6 );
63
70
}
64
71
@@ -72,34 +79,94 @@ void withPostProcessorForMethod() {
72
79
AnnotatedMethodBean proxy = bf .getBean (AnnotatedMethodBean .class );
73
80
AnnotatedMethodBean target = (AnnotatedMethodBean ) AopProxyUtils .getSingletonTarget (proxy );
74
81
75
- assertThatIllegalStateException ().isThrownBy (() -> proxy .retryOperation ().block ())
82
+ assertThatIllegalStateException ()
83
+ .isThrownBy (() -> proxy .retryOperation ().block ())
76
84
.satisfies (isRetryExhaustedException ())
77
- .withCauseInstanceOf (IOException .class ).havingCause ().withMessage ("6" );
85
+ .havingCause ()
86
+ .isInstanceOf (IOException .class )
87
+ .withMessage ("6" );
78
88
assertThat (target .counter .get ()).isEqualTo (6 );
79
89
}
80
90
81
91
@ 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 ();
89
94
AnnotatedClassBean target = (AnnotatedClassBean ) AopProxyUtils .getSingletonTarget (proxy );
90
95
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".
92
101
.satisfies (isReactiveException ())
93
- .withCauseInstanceOf (IOException .class ).havingCause ().withMessage ("3" );
102
+ .havingCause ()
103
+ .isInstanceOf (IOException .class )
104
+ .withMessage ("3" );
105
+ // 1 initial attempt + 2 retries
94
106
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
98
120
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 ())
100
166
.satisfies (isRetryExhaustedException ())
101
167
.withCauseInstanceOf (IOException .class );
102
- assertThat (target .counter .get ()).isEqualTo (6 );
168
+ // 1 initial attempt + 1 retry
169
+ assertThat (target .counter .get ()).isEqualTo (2 );
103
170
}
104
171
105
172
@ Test
@@ -113,9 +180,12 @@ void adaptReactiveResultWithMinimalRetrySpec() {
113
180
MinimalRetryBean proxy = (MinimalRetryBean ) pf .getProxy ();
114
181
115
182
// 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 ())
117
185
.satisfies (isRetryExhaustedException ())
118
- .withCauseInstanceOf (IOException .class ).havingCause ().withMessage ("2" );
186
+ .havingCause ()
187
+ .isInstanceOf (IOException .class )
188
+ .withMessage ("2" );
119
189
assertThat (target .counter .get ()).isEqualTo (2 );
120
190
}
121
191
@@ -129,9 +199,12 @@ void adaptReactiveResultWithZeroDelayAndJitter() {
129
199
new MethodRetrySpec ((m , t ) -> true , 3 , Duration .ZERO , Duration .ofMillis (10 ), 2.0 , Duration .ofMillis (100 ))));
130
200
ZeroDelayJitterBean proxy = (ZeroDelayJitterBean ) pf .getProxy ();
131
201
132
- assertThatIllegalStateException ().isThrownBy (() -> proxy .retryOperation ().block ())
202
+ assertThatIllegalStateException ()
203
+ .isThrownBy (() -> proxy .retryOperation ().block ())
133
204
.satisfies (isRetryExhaustedException ())
134
- .withCauseInstanceOf (IOException .class ).havingCause ().withMessage ("4" );
205
+ .havingCause ()
206
+ .isInstanceOf (IOException .class )
207
+ .withMessage ("4" );
135
208
assertThat (target .counter .get ()).isEqualTo (4 );
136
209
}
137
210
@@ -145,9 +218,12 @@ void adaptReactiveResultWithJitterGreaterThanDelay() {
145
218
new MethodRetrySpec ((m , t ) -> true , 3 , Duration .ofMillis (5 ), Duration .ofMillis (20 ), 1.5 , Duration .ofMillis (50 ))));
146
219
JitterGreaterThanDelayBean proxy = (JitterGreaterThanDelayBean ) pf .getProxy ();
147
220
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" );
151
227
assertThat (target .counter .get ()).isEqualTo (4 );
152
228
}
153
229
@@ -161,9 +237,12 @@ void adaptReactiveResultWithFluxMultiValue() {
161
237
new MethodRetrySpec ((m , t ) -> true , 3 , Duration .ofMillis (10 ), Duration .ofMillis (5 ), 2.0 , Duration .ofMillis (100 ))));
162
238
FluxMultiValueBean proxy = (FluxMultiValueBean ) pf .getProxy ();
163
239
164
- assertThatIllegalStateException ().isThrownBy (() -> proxy .retryOperation ().blockFirst ())
240
+ assertThatIllegalStateException ()
241
+ .isThrownBy (() -> proxy .retryOperation ().blockFirst ())
165
242
.satisfies (isRetryExhaustedException ())
166
- .withCauseInstanceOf (IOException .class ).havingCause ().withMessage ("4" );
243
+ .havingCause ()
244
+ .isInstanceOf (IOException .class )
245
+ .withMessage ("4" );
167
246
assertThat (target .counter .get ()).isEqualTo (4 );
168
247
}
169
248
@@ -184,28 +263,41 @@ void adaptReactiveResultWithSuccessfulOperation() {
184
263
}
185
264
186
265
@ 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 ();
190
269
ProxyFactory pf = new ProxyFactory ();
191
270
pf .setTarget (target );
192
271
pf .addAdvice (new SimpleRetryInterceptor (
193
272
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 ();
195
274
196
- assertThatIllegalStateException ().isThrownBy (() -> proxy .retryOperation ().block ())
275
+ assertThatIllegalStateException ()
276
+ .isThrownBy (() -> proxy .retryOperation ().block ())
197
277
.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
199
282
assertThat (target .counter .get ()).isEqualTo (4 );
200
283
}
201
284
202
285
203
286
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" );
205
288
}
206
289
207
290
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 );
209
301
}
210
302
211
303
@@ -238,26 +330,40 @@ public Mono<Object> retryOperation() {
238
330
239
331
@ Retryable (delay = 10 , jitter = 5 , multiplier = 2.0 , maxDelay = 40 ,
240
332
includes = IOException .class , excludes = AccessDeniedException .class ,
241
- predicate = CustomPredicate .class )
333
+ predicate = IOException3Predicate .class )
242
334
static class AnnotatedClassBean {
243
335
244
336
AtomicInteger counter = new AtomicInteger ();
245
337
246
- public Mono <Object > retryOperation () {
338
+ public Mono <Object > ioOperation () {
247
339
return Mono .fromCallable (() -> {
248
340
counter .incrementAndGet ();
249
341
throw new IOException (counter .toString ());
250
342
});
251
343
}
252
344
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 () {
254
353
return Mono .fromCallable (() -> {
255
354
counter .incrementAndGet ();
256
355
throw new AccessDeniedException (counter .toString ());
257
356
});
258
357
}
259
358
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 )
261
367
public Flux <Object > overrideOperation () {
262
368
return Flux .from (Mono .fromCallable (() -> {
263
369
counter .incrementAndGet ();
@@ -267,11 +373,11 @@ public Flux<Object> overrideOperation() {
267
373
}
268
374
269
375
270
- private static class CustomPredicate implements MethodRetryPredicate {
376
+ private static class IOException3Predicate implements MethodRetryPredicate {
271
377
272
378
@ Override
273
379
public boolean shouldRetry (Method method , Throwable throwable ) {
274
- return !"3" .equals (throwable .getMessage ());
380
+ return !( throwable . getClass () == IOException . class && "3" .equals (throwable .getMessage () ));
275
381
}
276
382
}
277
383
@@ -343,14 +449,14 @@ public Mono<String> retryOperation() {
343
449
}
344
450
345
451
346
- static class ImmediateFailureBean {
452
+ static class AlwaysFailsBean {
347
453
348
454
AtomicInteger counter = new AtomicInteger ();
349
455
350
456
public Mono <Object > retryOperation () {
351
457
return Mono .fromCallable (() -> {
352
458
counter .incrementAndGet ();
353
- throw new RuntimeException ( "immediate failure " );
459
+ throw new NumberFormatException ( "always fails " );
354
460
});
355
461
}
356
462
}
0 commit comments