Skip to content

Commit 2459009

Browse files
committed
Rename maxAttempts to maxRetries in @⁠Retryable and RetryPolicy
Prior to this commit, the maximum number of retry attempts was configured via @⁠Retryable(maxAttempts = ...), RetryPolicy.withMaxAttempts(), and RetryPolicy.Builder.maxAttempts(). However, this led to confusion for developers who were unsure if "max attempts" referred to the "total attempts" (i.e., initial attempt plus retry attempts) or only the "retry attempts". To improve the programming model, this commit renames maxAttempts to maxRetries in @⁠Retryable and RetryPolicy.Builder and renames RetryPolicy.withMaxAttempts() to RetryPolicy.withMaxRetries(). In addition, this commit updates the documentation to consistently point out that total attempts = 1 initial attempt + maxRetries attempts. Closes spring-projectsgh-35772
1 parent 771517d commit 2459009

12 files changed

Lines changed: 134 additions & 97 deletions

File tree

framework-docs/modules/ROOT/pages/core/resilience.adoc

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,19 @@ public void sendNotification() {
2323
}
2424
----
2525

26-
By default, the method invocation will be retried for any exception thrown: with at most 3
27-
retry attempts after an initial failure, and a delay of 1 second between attempts.
26+
By default, the method invocation will be retried for any exception thrown: with at most
27+
3 retry attempts (`maxRetries = 3`) after an initial failure, and a delay of 1 second
28+
between attempts.
29+
30+
[NOTE]
31+
====
32+
A `@Retryable` method will be invoked at least once and retried at most `maxRetries`
33+
times, where `maxRetries` is the maximum number of retry attempts. Specifically,
34+
`total attempts = 1 initial attempt + maxRetries attempts`.
35+
36+
For example, if `maxRetries` is set to `4`, the `@Retryable` method will be invoked at
37+
least once and at most 5 times.
38+
====
2839

2940
This can be specifically adapted for every method if necessary — for example, by narrowing
3041
the exceptions to retry via the `includes` and `excludes` attributes. The supplied
@@ -53,13 +64,13 @@ Custom predicates can be combined with `includes` and `excludes`; however, custo
5364
predicates will always be applied after `includes` and `excludes` have been applied.
5465
====
5566

56-
Or for 5 retry attempts and an exponential back-off strategy with a bit of jitter:
67+
Or for 4 retry attempts and an exponential back-off strategy with a bit of jitter:
5768

5869
[source,java,indent=0,subs="verbatim,quotes"]
5970
----
6071
@Retryable(
6172
includes = MessageDeliveryException.class,
62-
maxAttempts = 5,
73+
maxRetries = 4,
6374
delay = 100,
6475
jitter = 10,
6576
multiplier = 2,
@@ -74,7 +85,7 @@ type, decorating the pipeline with Reactor's retry capabilities:
7485

7586
[source,java,indent=0,subs="verbatim,quotes"]
7687
----
77-
@Retryable(maxAttempts = 5, delay = 100)
88+
@Retryable(maxRetries = 4, delay = 100)
7889
public Mono<Void> sendNotification() {
7990
return Mono.from(...); // <1>
8091
}
@@ -168,20 +179,31 @@ configured {spring-framework-api}/core/retry/RetryPolicy.html[`RetryPolicy`].
168179
----
169180
<1> Implicitly uses `RetryPolicy.withDefaults()`.
170181

171-
By default, a retryable operation will be retried for any exception thrown: with at most 3
172-
retry attempts after an initial failure, and a delay of 1 second between attempts.
182+
By default, a retryable operation will be retried for any exception thrown: with at most
183+
3 retry attempts (`maxRetries = 3`) after an initial failure, and a delay of 1 second
184+
between attempts.
173185

174186
If you only need to customize the number of retry attempts, you can use the
175-
`RetryPolicy.withMaxAttempts()` factory method as demonstrated below.
187+
`RetryPolicy.withMaxRetries()` factory method as demonstrated below.
188+
189+
[NOTE]
190+
====
191+
A retryable operation will be executed at least once and retried at most `maxRetries`
192+
times, where `maxRetries` is the maximum number of retry attempts. Specifically,
193+
`total attempts = 1 initial attempt + maxRetries attempts`.
194+
195+
For example, if `maxRetries` is set to `4`, the retryable operation will be invoked at
196+
least once and at most 5 times.
197+
====
176198

177199
[source,java,indent=0,subs="verbatim,quotes"]
178200
----
179-
var retryTemplate = new RetryTemplate(RetryPolicy.withMaxAttempts(5)); // <1>
201+
var retryTemplate = new RetryTemplate(RetryPolicy.withMaxRetries(4)); // <1>
180202
181203
retryTemplate.execute(
182204
() -> jmsClient.destination("notifications").send(...));
183205
----
184-
<1> Explicitly uses `RetryPolicy.withMaxAttempts(5)`.
206+
<1> Explicitly uses `RetryPolicy.withMaxRetries(4)`.
185207

186208
If you need to narrow the types of exceptions to retry, that can be achieved via the
187209
`includes()` and `excludes()` builder methods. The supplied exception types will be
@@ -213,14 +235,14 @@ Custom predicates can be combined with `includes` and `excludes`; however, custo
213235
predicates will always be applied after `includes` and `excludes` have been applied.
214236
====
215237

216-
The following example demonstrates how to configure a `RetryPolicy` with 5 retry attempts
238+
The following example demonstrates how to configure a `RetryPolicy` with 4 retry attempts
217239
and an exponential back-off strategy with a bit of jitter.
218240

219241
[source,java,indent=0,subs="verbatim,quotes"]
220242
----
221243
var retryPolicy = RetryPolicy.builder()
222244
.includes(MessageDeliveryException.class)
223-
.maxAttempts(5)
245+
.maxRetries(4)
224246
.delay(Duration.ofMillis(100))
225247
.jitter(Duration.ofMillis(10))
226248
.multiplier(2)

spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ private class RetryAnnotationInterceptor extends AbstractRetryInterceptor {
9898
retrySpec = new MethodRetrySpec(
9999
Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()),
100100
instantiatePredicate(retryable.predicate()),
101-
parseLong(retryable.maxAttempts(), retryable.maxAttemptsString()),
101+
parseLong(retryable.maxRetries(), retryable.maxRetriesString()),
102102
parseDuration(retryable.delay(), retryable.delayString(), timeUnit),
103103
parseDuration(retryable.jitter(), retryable.jitterString(), timeUnit),
104104
parseDouble(retryable.multiplier(), retryable.multiplierString()),

spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,18 +106,21 @@
106106
Class<? extends MethodRetryPredicate> predicate() default MethodRetryPredicate.class;
107107

108108
/**
109-
* The maximum number of retry attempts, in addition to the initial invocation.
109+
* The maximum number of retry attempts.
110+
* <p>Note that {@code total attempts = 1 initial attempt + maxRetries attempts}.
111+
* Thus, if {@code maxRetries} is set to 4, the annotated method will be invoked
112+
* at least once and at most 5 times.
110113
* <p>The default is 3.
111114
*/
112-
long maxAttempts() default 3;
115+
long maxRetries() default 3;
113116

114117
/**
115118
* The maximum number of retry attempts, as a configurable String.
116-
* <p>A non-empty value specified here overrides the {@link #maxAttempts()} attribute.
119+
* <p>A non-empty value specified here overrides the {@link #maxRetries()} attribute.
117120
* <p>This supports Spring-style "${...}" placeholders as well as SpEL expressions.
118-
* @see #maxAttempts()
121+
* @see #maxRetries()
119122
*/
120-
String maxAttemptsString() default "";
123+
String maxRetriesString() default "";
121124

122125
/**
123126
* The base delay after the initial invocation. If a multiplier is specified,

spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public AbstractRetryInterceptor() {
9393
.includes(spec.includes())
9494
.excludes(spec.excludes())
9595
.predicate(spec.predicate().forMethod(method))
96-
.maxAttempts(spec.maxAttempts())
96+
.maxRetries(spec.maxRetries())
9797
.delay(spec.delay())
9898
.jitter(spec.jitter())
9999
.multiplier(spec.multiplier())
@@ -137,7 +137,7 @@ public static Object adaptReactiveResult(
137137
Object result, ReactiveAdapter adapter, MethodRetrySpec spec, Method method) {
138138

139139
Publisher<?> publisher = adapter.toPublisher(result);
140-
Retry retry = Retry.backoff(spec.maxAttempts(), spec.delay())
140+
Retry retry = Retry.backoff(spec.maxRetries(), spec.delay())
141141
.jitter(calculateJitterFactor(spec))
142142
.multiplier(spec.multiplier())
143143
.maxBackoff(spec.maxDelay())

spring-context/src/main/java/org/springframework/resilience/retry/MethodRetrySpec.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* @param includes applicable exception types to attempt a retry for
3333
* @param excludes non-applicable exception types to avoid a retry for
3434
* @param predicate a predicate for filtering exceptions from applicable methods
35-
* @param maxAttempts the maximum number of retry attempts
35+
* @param maxRetries the maximum number of retry attempts
3636
* @param delay the base delay after the initial invocation
3737
* @param jitter a jitter value for the next retry attempt
3838
* @param multiplier a multiplier for a delay for the next retry attempt
@@ -45,20 +45,20 @@ public record MethodRetrySpec(
4545
Collection<Class<? extends Throwable>> includes,
4646
Collection<Class<? extends Throwable>> excludes,
4747
MethodRetryPredicate predicate,
48-
long maxAttempts,
48+
long maxRetries,
4949
Duration delay,
5050
Duration jitter,
5151
double multiplier,
5252
Duration maxDelay) {
5353

54-
public MethodRetrySpec(MethodRetryPredicate predicate, long maxAttempts, Duration delay) {
55-
this(predicate, maxAttempts, delay, Duration.ZERO, 1.0, Duration.ofMillis(Long.MAX_VALUE));
54+
public MethodRetrySpec(MethodRetryPredicate predicate, long maxRetries, Duration delay) {
55+
this(predicate, maxRetries, delay, Duration.ZERO, 1.0, Duration.ofMillis(Long.MAX_VALUE));
5656
}
5757

58-
public MethodRetrySpec(MethodRetryPredicate predicate, long maxAttempts, Duration delay,
58+
public MethodRetrySpec(MethodRetryPredicate predicate, long maxRetries, Duration delay,
5959
Duration jitter, double multiplier, Duration maxDelay) {
6060

61-
this(Collections.emptyList(), Collections.emptyList(), predicate, maxAttempts, delay,
61+
this(Collections.emptyList(), Collections.emptyList(), predicate, maxRetries, delay,
6262
jitter, multiplier, maxDelay);
6363
}
6464

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,15 @@ void withPostProcessorForClassWithMethodLevelOverride() {
189189

190190
@Test
191191
void adaptReactiveResultWithMinimalRetrySpec() {
192-
// Test minimal retry configuration: maxAttempts=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
192+
// Test minimal retry configuration: maxRetries=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
193193
MinimalRetryBean target = new MinimalRetryBean();
194194
ProxyFactory pf = new ProxyFactory();
195195
pf.setTarget(target);
196196
pf.addAdvice(new SimpleRetryInterceptor(
197197
new MethodRetrySpec((m, t) -> true, 1, Duration.ZERO, Duration.ZERO, 1.0, Duration.ZERO)));
198198
MinimalRetryBean proxy = (MinimalRetryBean) pf.getProxy();
199199

200-
// Should execute only 2 times, because maxAttempts=1 means 1 call + 1 retry
200+
// Should execute only 2 times, because maxRetries=1 means 1 call + 1 retry
201201
assertThatIllegalStateException()
202202
.isThrownBy(() -> proxy.retryOperation().block())
203203
.satisfies(isRetryExhaustedException())
@@ -209,15 +209,15 @@ void adaptReactiveResultWithMinimalRetrySpec() {
209209

210210
@Test
211211
void adaptReactiveResultWithZeroAttempts() {
212-
// Test minimal retry configuration: maxAttempts=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
212+
// Test minimal retry configuration: maxRetries=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
213213
MinimalRetryBean target = new MinimalRetryBean();
214214
ProxyFactory pf = new ProxyFactory();
215215
pf.setTarget(target);
216216
pf.addAdvice(new SimpleRetryInterceptor(
217217
new MethodRetrySpec((m, t) -> true, 0, Duration.ZERO, Duration.ZERO, 1.0, Duration.ZERO)));
218218
MinimalRetryBean proxy = (MinimalRetryBean) pf.getProxy();
219219

220-
// Should execute only 1 time, because maxAttempts=0 means initial call only
220+
// Should execute only 1 time, because maxRetries=0 means initial call only
221221
assertThatIllegalStateException()
222222
.isThrownBy(() -> proxy.retryOperation().block())
223223
.satisfies(isRetryExhaustedException())
@@ -302,7 +302,7 @@ void adaptReactiveResultWithSuccessfulOperation() {
302302

303303
@Test
304304
void adaptReactiveResultWithAlwaysFailingOperation() {
305-
// Test "always fails" case, ensuring retry mechanism stops after maxAttempts (3)
305+
// Test "always fails" case, ensuring retry mechanism stops after maxRetries (3)
306306
AlwaysFailsBean target = new AlwaysFailsBean();
307307
ProxyFactory pf = new ProxyFactory();
308308
pf.setTarget(target);
@@ -356,7 +356,7 @@ static class AnnotatedMethodBean {
356356

357357
AtomicInteger counter = new AtomicInteger();
358358

359-
@Retryable(maxAttempts = 5, delay = 10)
359+
@Retryable(maxRetries = 5, delay = 10)
360360
public Mono<Object> retryOperation() {
361361
return Mono.fromCallable(() -> {
362362
counter.incrementAndGet();
@@ -411,7 +411,7 @@ public Mono<Object> arithmeticOperation() {
411411
});
412412
}
413413

414-
@Retryable(includes = IOException.class, maxAttempts = 1, delay = 10)
414+
@Retryable(includes = IOException.class, maxRetries = 1, delay = 10)
415415
public Flux<Object> overrideOperation() {
416416
return Flux.from(Mono.fromCallable(() -> {
417417
counter.incrementAndGet();

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ void withPostProcessorForClassWithStrings() {
218218
props.setProperty("jitter", "5");
219219
props.setProperty("multiplier", "2.0");
220220
props.setProperty("maxDelay", "40");
221-
props.setProperty("limitedAttempts", "1");
221+
props.setProperty("limitedRetries", "1");
222222

223223
GenericApplicationContext ctx = new GenericApplicationContext();
224224
ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("props", props));
@@ -246,7 +246,7 @@ void withPostProcessorForClassWithZeroAttempts() {
246246
props.setProperty("jitter", "5");
247247
props.setProperty("multiplier", "2.0");
248248
props.setProperty("maxDelay", "40");
249-
props.setProperty("limitedAttempts", "0");
249+
props.setProperty("limitedRetries", "0");
250250

251251
GenericApplicationContext ctx = new GenericApplicationContext();
252252
ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("props", props));
@@ -321,7 +321,7 @@ static class AnnotatedMethodBean {
321321

322322
int counter = 0;
323323

324-
@Retryable(maxAttempts = 5, delay = 10)
324+
@Retryable(maxRetries = 5, delay = 10)
325325
public void retryOperation() throws IOException {
326326
counter++;
327327
throw new IOException(Integer.toString(counter));
@@ -333,7 +333,7 @@ static class AnnotatedMethodBeanWithInterface implements AnnotatedInterface {
333333

334334
int counter = 0;
335335

336-
@Retryable(maxAttempts = 5, delay = 10)
336+
@Retryable(maxRetries = 5, delay = 10)
337337
@Override
338338
public void retryOperation() throws IOException {
339339
counter++;
@@ -344,7 +344,7 @@ public void retryOperation() throws IOException {
344344

345345
interface AnnotatedInterface {
346346

347-
@Retryable(maxAttempts = 5, delay = 10)
347+
@Retryable(maxRetries = 5, delay = 10)
348348
void retryOperation() throws IOException;
349349
}
350350

@@ -374,7 +374,7 @@ public void otherOperation() throws IOException {
374374
throw new AccessDeniedException(Integer.toString(counter));
375375
}
376376

377-
@Retryable(value = IOException.class, maxAttempts = 1, delay = 10)
377+
@Retryable(value = IOException.class, maxRetries = 1, delay = 10)
378378
public void overrideOperation() throws IOException {
379379
counter++;
380380
throw new AccessDeniedException(Integer.toString(counter));
@@ -403,7 +403,7 @@ public void otherOperation() throws IOException {
403403
throw new AccessDeniedException(Integer.toString(counter));
404404
}
405405

406-
@Retryable(value = IOException.class, maxAttemptsString = "${limitedAttempts}", delayString = "10ms")
406+
@Retryable(value = IOException.class, maxRetriesString = "${limitedRetries}", delayString = "10ms")
407407
public void overrideOperation() throws IOException {
408408
counter++;
409409
throw new AccessDeniedException(Integer.toString(counter));
@@ -422,7 +422,7 @@ static class ConcurrencyLimitAnnotatedBean {
422422
volatile String lastThreadName;
423423

424424
@ConcurrencyLimit(1)
425-
@Retryable(maxAttempts = 2, delay = 10)
425+
@Retryable(maxRetries = 2, delay = 10)
426426
public void retryOperation() throws IOException, InterruptedException {
427427
if (current.incrementAndGet() > 1) {
428428
throw new IllegalStateException();
@@ -443,7 +443,7 @@ static class AsyncAnnotatedBean {
443443
AtomicInteger counter = new AtomicInteger();
444444

445445
@Async
446-
@Retryable(maxAttempts = 2, delay = 10)
446+
@Retryable(maxRetries = 2, delay = 10)
447447
public CompletableFuture<Void> retryOperation() {
448448
throw new IllegalStateException(Integer.toString(counter.incrementAndGet()));
449449
}

0 commit comments

Comments
 (0)