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 gh-35772
This commit is contained in:
Sam Brannen
2025-11-10 11:15:25 +01:00
parent 771517dc36
commit 24590092ef
12 changed files with 134 additions and 97 deletions
@@ -189,7 +189,7 @@ class ReactiveRetryInterceptorTests {
@Test
void adaptReactiveResultWithMinimalRetrySpec() {
// Test minimal retry configuration: maxAttempts=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
// Test minimal retry configuration: maxRetries=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
MinimalRetryBean target = new MinimalRetryBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
@@ -197,7 +197,7 @@ class ReactiveRetryInterceptorTests {
new MethodRetrySpec((m, t) -> true, 1, Duration.ZERO, Duration.ZERO, 1.0, Duration.ZERO)));
MinimalRetryBean proxy = (MinimalRetryBean) pf.getProxy();
// Should execute only 2 times, because maxAttempts=1 means 1 call + 1 retry
// Should execute only 2 times, because maxRetries=1 means 1 call + 1 retry
assertThatIllegalStateException()
.isThrownBy(() -> proxy.retryOperation().block())
.satisfies(isRetryExhaustedException())
@@ -209,7 +209,7 @@ class ReactiveRetryInterceptorTests {
@Test
void adaptReactiveResultWithZeroAttempts() {
// Test minimal retry configuration: maxAttempts=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
// Test minimal retry configuration: maxRetries=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
MinimalRetryBean target = new MinimalRetryBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
@@ -217,7 +217,7 @@ class ReactiveRetryInterceptorTests {
new MethodRetrySpec((m, t) -> true, 0, Duration.ZERO, Duration.ZERO, 1.0, Duration.ZERO)));
MinimalRetryBean proxy = (MinimalRetryBean) pf.getProxy();
// Should execute only 1 time, because maxAttempts=0 means initial call only
// Should execute only 1 time, because maxRetries=0 means initial call only
assertThatIllegalStateException()
.isThrownBy(() -> proxy.retryOperation().block())
.satisfies(isRetryExhaustedException())
@@ -302,7 +302,7 @@ class ReactiveRetryInterceptorTests {
@Test
void adaptReactiveResultWithAlwaysFailingOperation() {
// Test "always fails" case, ensuring retry mechanism stops after maxAttempts (3)
// Test "always fails" case, ensuring retry mechanism stops after maxRetries (3)
AlwaysFailsBean target = new AlwaysFailsBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
@@ -356,7 +356,7 @@ class ReactiveRetryInterceptorTests {
AtomicInteger counter = new AtomicInteger();
@Retryable(maxAttempts = 5, delay = 10)
@Retryable(maxRetries = 5, delay = 10)
public Mono<Object> retryOperation() {
return Mono.fromCallable(() -> {
counter.incrementAndGet();
@@ -411,7 +411,7 @@ class ReactiveRetryInterceptorTests {
});
}
@Retryable(includes = IOException.class, maxAttempts = 1, delay = 10)
@Retryable(includes = IOException.class, maxRetries = 1, delay = 10)
public Flux<Object> overrideOperation() {
return Flux.from(Mono.fromCallable(() -> {
counter.incrementAndGet();
@@ -218,7 +218,7 @@ class RetryInterceptorTests {
props.setProperty("jitter", "5");
props.setProperty("multiplier", "2.0");
props.setProperty("maxDelay", "40");
props.setProperty("limitedAttempts", "1");
props.setProperty("limitedRetries", "1");
GenericApplicationContext ctx = new GenericApplicationContext();
ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("props", props));
@@ -246,7 +246,7 @@ class RetryInterceptorTests {
props.setProperty("jitter", "5");
props.setProperty("multiplier", "2.0");
props.setProperty("maxDelay", "40");
props.setProperty("limitedAttempts", "0");
props.setProperty("limitedRetries", "0");
GenericApplicationContext ctx = new GenericApplicationContext();
ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("props", props));
@@ -321,7 +321,7 @@ class RetryInterceptorTests {
int counter = 0;
@Retryable(maxAttempts = 5, delay = 10)
@Retryable(maxRetries = 5, delay = 10)
public void retryOperation() throws IOException {
counter++;
throw new IOException(Integer.toString(counter));
@@ -333,7 +333,7 @@ class RetryInterceptorTests {
int counter = 0;
@Retryable(maxAttempts = 5, delay = 10)
@Retryable(maxRetries = 5, delay = 10)
@Override
public void retryOperation() throws IOException {
counter++;
@@ -344,7 +344,7 @@ class RetryInterceptorTests {
interface AnnotatedInterface {
@Retryable(maxAttempts = 5, delay = 10)
@Retryable(maxRetries = 5, delay = 10)
void retryOperation() throws IOException;
}
@@ -374,7 +374,7 @@ class RetryInterceptorTests {
throw new AccessDeniedException(Integer.toString(counter));
}
@Retryable(value = IOException.class, maxAttempts = 1, delay = 10)
@Retryable(value = IOException.class, maxRetries = 1, delay = 10)
public void overrideOperation() throws IOException {
counter++;
throw new AccessDeniedException(Integer.toString(counter));
@@ -403,7 +403,7 @@ class RetryInterceptorTests {
throw new AccessDeniedException(Integer.toString(counter));
}
@Retryable(value = IOException.class, maxAttemptsString = "${limitedAttempts}", delayString = "10ms")
@Retryable(value = IOException.class, maxRetriesString = "${limitedRetries}", delayString = "10ms")
public void overrideOperation() throws IOException {
counter++;
throw new AccessDeniedException(Integer.toString(counter));
@@ -422,7 +422,7 @@ class RetryInterceptorTests {
volatile String lastThreadName;
@ConcurrencyLimit(1)
@Retryable(maxAttempts = 2, delay = 10)
@Retryable(maxRetries = 2, delay = 10)
public void retryOperation() throws IOException, InterruptedException {
if (current.incrementAndGet() > 1) {
throw new IllegalStateException();
@@ -443,7 +443,7 @@ class RetryInterceptorTests {
AtomicInteger counter = new AtomicInteger();
@Async
@Retryable(maxAttempts = 2, delay = 10)
@Retryable(maxRetries = 2, delay = 10)
public CompletableFuture<Void> retryOperation() {
throw new IllegalStateException(Integer.toString(counter.incrementAndGet()));
}