From 24590092efa313cd56b3a9a0752a5b54f2fe03a1 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:23:45 +0100 Subject: [PATCH] =?UTF-8?q?Rename=20maxAttempts=20to=20maxRetries=20in=20@?= =?UTF-8?q?=E2=81=A0Retryable=20and=20RetryPolicy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../modules/ROOT/pages/core/resilience.adoc | 46 ++++++++++---- .../RetryAnnotationBeanPostProcessor.java | 2 +- .../resilience/annotation/Retryable.java | 13 ++-- .../retry/AbstractRetryInterceptor.java | 4 +- .../resilience/retry/MethodRetrySpec.java | 12 ++-- .../ReactiveRetryInterceptorTests.java | 14 ++--- .../resilience/RetryInterceptorTests.java | 18 +++--- .../core/retry/RetryPolicy.java | 60 +++++++++++-------- .../core/retry/RetryTemplate.java | 6 +- ...s.java => MaxRetriesRetryPolicyTests.java} | 20 +++---- .../core/retry/RetryPolicyTests.java | 22 +++---- .../core/retry/RetryTemplateTests.java | 14 ++--- 12 files changed, 134 insertions(+), 97 deletions(-) rename spring-core/src/test/java/org/springframework/core/retry/{MaxAttemptsRetryPolicyTests.java => MaxRetriesRetryPolicyTests.java} (91%) diff --git a/framework-docs/modules/ROOT/pages/core/resilience.adoc b/framework-docs/modules/ROOT/pages/core/resilience.adoc index 4004064800f..50432b9fec7 100644 --- a/framework-docs/modules/ROOT/pages/core/resilience.adoc +++ b/framework-docs/modules/ROOT/pages/core/resilience.adoc @@ -23,8 +23,19 @@ public void sendNotification() { } ---- -By default, the method invocation will be retried for any exception thrown: with at most 3 -retry attempts after an initial failure, and a delay of 1 second between attempts. +By default, the method invocation will be retried for any exception thrown: with at most +3 retry attempts (`maxRetries = 3`) after an initial failure, and a delay of 1 second +between attempts. + +[NOTE] +==== +A `@Retryable` method will be invoked at least once and retried at most `maxRetries` +times, where `maxRetries` is the maximum number of retry attempts. Specifically, +`total attempts = 1 initial attempt + maxRetries attempts`. + +For example, if `maxRetries` is set to `4`, the `@Retryable` method will be invoked at +least once and at most 5 times. +==== This can be specifically adapted for every method if necessary — for example, by narrowing 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 predicates will always be applied after `includes` and `excludes` have been applied. ==== -Or for 5 retry attempts and an exponential back-off strategy with a bit of jitter: +Or for 4 retry attempts and an exponential back-off strategy with a bit of jitter: [source,java,indent=0,subs="verbatim,quotes"] ---- @Retryable( includes = MessageDeliveryException.class, - maxAttempts = 5, + maxRetries = 4, delay = 100, jitter = 10, multiplier = 2, @@ -74,7 +85,7 @@ type, decorating the pipeline with Reactor's retry capabilities: [source,java,indent=0,subs="verbatim,quotes"] ---- -@Retryable(maxAttempts = 5, delay = 100) +@Retryable(maxRetries = 4, delay = 100) public Mono sendNotification() { return Mono.from(...); // <1> } @@ -168,20 +179,31 @@ configured {spring-framework-api}/core/retry/RetryPolicy.html[`RetryPolicy`]. ---- <1> Implicitly uses `RetryPolicy.withDefaults()`. -By default, a retryable operation will be retried for any exception thrown: with at most 3 -retry attempts after an initial failure, and a delay of 1 second between attempts. +By default, a retryable operation will be retried for any exception thrown: with at most +3 retry attempts (`maxRetries = 3`) after an initial failure, and a delay of 1 second +between attempts. If you only need to customize the number of retry attempts, you can use the -`RetryPolicy.withMaxAttempts()` factory method as demonstrated below. +`RetryPolicy.withMaxRetries()` factory method as demonstrated below. + +[NOTE] +==== +A retryable operation will be executed at least once and retried at most `maxRetries` +times, where `maxRetries` is the maximum number of retry attempts. Specifically, +`total attempts = 1 initial attempt + maxRetries attempts`. + +For example, if `maxRetries` is set to `4`, the retryable operation will be invoked at +least once and at most 5 times. +==== [source,java,indent=0,subs="verbatim,quotes"] ---- - var retryTemplate = new RetryTemplate(RetryPolicy.withMaxAttempts(5)); // <1> + var retryTemplate = new RetryTemplate(RetryPolicy.withMaxRetries(4)); // <1> retryTemplate.execute( () -> jmsClient.destination("notifications").send(...)); ---- -<1> Explicitly uses `RetryPolicy.withMaxAttempts(5)`. +<1> Explicitly uses `RetryPolicy.withMaxRetries(4)`. If you need to narrow the types of exceptions to retry, that can be achieved via the `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 predicates will always be applied after `includes` and `excludes` have been applied. ==== -The following example demonstrates how to configure a `RetryPolicy` with 5 retry attempts +The following example demonstrates how to configure a `RetryPolicy` with 4 retry attempts and an exponential back-off strategy with a bit of jitter. [source,java,indent=0,subs="verbatim,quotes"] ---- var retryPolicy = RetryPolicy.builder() .includes(MessageDeliveryException.class) - .maxAttempts(5) + .maxRetries(4) .delay(Duration.ofMillis(100)) .jitter(Duration.ofMillis(10)) .multiplier(2) diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java index e9d80657d41..3b402856e80 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java @@ -98,7 +98,7 @@ public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAd retrySpec = new MethodRetrySpec( Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()), instantiatePredicate(retryable.predicate()), - parseLong(retryable.maxAttempts(), retryable.maxAttemptsString()), + parseLong(retryable.maxRetries(), retryable.maxRetriesString()), parseDuration(retryable.delay(), retryable.delayString(), timeUnit), parseDuration(retryable.jitter(), retryable.jitterString(), timeUnit), parseDouble(retryable.multiplier(), retryable.multiplierString()), diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java b/spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java index 10a42093040..b9851b25852 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java @@ -106,18 +106,21 @@ public @interface Retryable { Class predicate() default MethodRetryPredicate.class; /** - * The maximum number of retry attempts, in addition to the initial invocation. + * The maximum number of retry attempts. + *

Note that {@code total attempts = 1 initial attempt + maxRetries attempts}. + * Thus, if {@code maxRetries} is set to 4, the annotated method will be invoked + * at least once and at most 5 times. *

The default is 3. */ - long maxAttempts() default 3; + long maxRetries() default 3; /** * The maximum number of retry attempts, as a configurable String. - *

A non-empty value specified here overrides the {@link #maxAttempts()} attribute. + *

A non-empty value specified here overrides the {@link #maxRetries()} attribute. *

This supports Spring-style "${...}" placeholders as well as SpEL expressions. - * @see #maxAttempts() + * @see #maxRetries() */ - String maxAttemptsString() default ""; + String maxRetriesString() default ""; /** * The base delay after the initial invocation. If a multiplier is specified, diff --git a/spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java b/spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java index d4a14fa3427..67964fb366e 100644 --- a/spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java +++ b/spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java @@ -93,7 +93,7 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor { .includes(spec.includes()) .excludes(spec.excludes()) .predicate(spec.predicate().forMethod(method)) - .maxAttempts(spec.maxAttempts()) + .maxRetries(spec.maxRetries()) .delay(spec.delay()) .jitter(spec.jitter()) .multiplier(spec.multiplier()) @@ -137,7 +137,7 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor { Object result, ReactiveAdapter adapter, MethodRetrySpec spec, Method method) { Publisher publisher = adapter.toPublisher(result); - Retry retry = Retry.backoff(spec.maxAttempts(), spec.delay()) + Retry retry = Retry.backoff(spec.maxRetries(), spec.delay()) .jitter(calculateJitterFactor(spec)) .multiplier(spec.multiplier()) .maxBackoff(spec.maxDelay()) diff --git a/spring-context/src/main/java/org/springframework/resilience/retry/MethodRetrySpec.java b/spring-context/src/main/java/org/springframework/resilience/retry/MethodRetrySpec.java index b6d73192109..f07d777f4db 100644 --- a/spring-context/src/main/java/org/springframework/resilience/retry/MethodRetrySpec.java +++ b/spring-context/src/main/java/org/springframework/resilience/retry/MethodRetrySpec.java @@ -32,7 +32,7 @@ import org.springframework.util.ExceptionTypeFilter; * @param includes applicable exception types to attempt a retry for * @param excludes non-applicable exception types to avoid a retry for * @param predicate a predicate for filtering exceptions from applicable methods - * @param maxAttempts the maximum number of retry attempts + * @param maxRetries the maximum number of retry attempts * @param delay the base delay after the initial invocation * @param jitter a jitter value for the next retry attempt * @param multiplier a multiplier for a delay for the next retry attempt @@ -45,20 +45,20 @@ public record MethodRetrySpec( Collection> includes, Collection> excludes, MethodRetryPredicate predicate, - long maxAttempts, + long maxRetries, Duration delay, Duration jitter, double multiplier, Duration maxDelay) { - public MethodRetrySpec(MethodRetryPredicate predicate, long maxAttempts, Duration delay) { - this(predicate, maxAttempts, delay, Duration.ZERO, 1.0, Duration.ofMillis(Long.MAX_VALUE)); + public MethodRetrySpec(MethodRetryPredicate predicate, long maxRetries, Duration delay) { + this(predicate, maxRetries, delay, Duration.ZERO, 1.0, Duration.ofMillis(Long.MAX_VALUE)); } - public MethodRetrySpec(MethodRetryPredicate predicate, long maxAttempts, Duration delay, + public MethodRetrySpec(MethodRetryPredicate predicate, long maxRetries, Duration delay, Duration jitter, double multiplier, Duration maxDelay) { - this(Collections.emptyList(), Collections.emptyList(), predicate, maxAttempts, delay, + this(Collections.emptyList(), Collections.emptyList(), predicate, maxRetries, delay, jitter, multiplier, maxDelay); } diff --git a/spring-context/src/test/java/org/springframework/resilience/ReactiveRetryInterceptorTests.java b/spring-context/src/test/java/org/springframework/resilience/ReactiveRetryInterceptorTests.java index a289b79112b..06184fa8fb3 100644 --- a/spring-context/src/test/java/org/springframework/resilience/ReactiveRetryInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/resilience/ReactiveRetryInterceptorTests.java @@ -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 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 overrideOperation() { return Flux.from(Mono.fromCallable(() -> { counter.incrementAndGet(); diff --git a/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java b/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java index 7e32ebdaa59..56eef09be4f 100644 --- a/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java @@ -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 retryOperation() { throw new IllegalStateException(Integer.toString(counter.incrementAndGet())); } diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java index eefe6c0ed52..ebc9278b4db 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java @@ -35,7 +35,7 @@ import org.springframework.util.backoff.FixedBackOff; * *

Also provides factory methods and a fluent builder API for creating retry * policies with common configurations. See {@link #withDefaults()}, - * {@link #withMaxAttempts(long)}, {@link #builder()}, and the configuration + * {@link #withMaxRetries(long)}, {@link #builder()}, and the configuration * options in {@link Builder} for details. * * @author Sam Brannen @@ -58,12 +58,15 @@ public interface RetryPolicy { /** * Get the {@link BackOff} strategy to use for this retry policy. *

Defaults to a fixed backoff of {@value Builder#DEFAULT_DELAY} milliseconds - * and maximum {@value Builder#DEFAULT_MAX_ATTEMPTS} retry attempts. + * and maximum {@value Builder#DEFAULT_MAX_RETRIES} retries. + *

Note that {@code total attempts = 1 initial attempt + maxRetries attempts}. + * Thus, when {@code maxRetries} is set to 3, a retryable operation will be + * invoked at least once and at most 4 times. * @return the {@code BackOff} strategy to use * @see FixedBackOff */ default BackOff getBackOff() { - return new FixedBackOff(Builder.DEFAULT_DELAY, Builder.DEFAULT_MAX_ATTEMPTS); + return new FixedBackOff(Builder.DEFAULT_DELAY, Builder.DEFAULT_MAX_RETRIES); } @@ -71,7 +74,10 @@ public interface RetryPolicy { * Create a {@link RetryPolicy} with default configuration. *

The returned policy applies to all exception types, uses a fixed backoff * of {@value Builder#DEFAULT_DELAY} milliseconds, and supports maximum - * {@value Builder#DEFAULT_MAX_ATTEMPTS} retry attempts. + * {@value Builder#DEFAULT_MAX_RETRIES} retries. + *

Note that {@code total attempts = 1 initial attempt + maxRetries attempts}. + * Thus, when {@code maxRetries} is set to 3, a retryable operation will be + * invoked at least once and at most 4 times. * @see FixedBackOff */ static RetryPolicy withDefaults() { @@ -80,16 +86,19 @@ public interface RetryPolicy { /** * Create a {@link RetryPolicy} configured with a maximum number of retry attempts. + *

Note that {@code total attempts = 1 initial attempt + maxRetries attempts}. + * Thus, if {@code maxRetries} is set to 4, a retryable operation will be invoked + * at least once and at most 5 times. *

The returned policy applies to all exception types and uses a fixed backoff * of {@value Builder#DEFAULT_DELAY} milliseconds. - * @param maxAttempts the maximum number of retry attempts; + * @param maxRetries the maximum number of retry attempts; * must be positive (or zero for no retry) - * @see Builder#maxAttempts(long) + * @see Builder#maxRetries(long) * @see FixedBackOff */ - static RetryPolicy withMaxAttempts(long maxAttempts) { - assertMaxAttemptsIsNotNegative(maxAttempts); - return builder().backOff(new FixedBackOff(Builder.DEFAULT_DELAY, maxAttempts)).build(); + static RetryPolicy withMaxRetries(long maxRetries) { + assertMaxRetriesIsNotNegative(maxRetries); + return builder().backOff(new FixedBackOff(Builder.DEFAULT_DELAY, maxRetries)).build(); } /** @@ -101,9 +110,9 @@ public interface RetryPolicy { } - private static void assertMaxAttemptsIsNotNegative(long maxAttempts) { - Assert.isTrue(maxAttempts >= 0, - () -> "Invalid maxAttempts (%d): must be positive or zero for no retry.".formatted(maxAttempts)); + private static void assertMaxRetriesIsNotNegative(long maxRetries) { + Assert.isTrue(maxRetries >= 0, + () -> "Invalid maxRetries (%d): must be positive or zero for no retry.".formatted(maxRetries)); } private static void assertIsNotNegative(String name, Duration duration) { @@ -124,9 +133,9 @@ public interface RetryPolicy { final class Builder { /** - * The default {@linkplain #maxAttempts(long) max attempts}: {@value}. + * The default {@linkplain #maxRetries(long) max retries}: {@value}. */ - public static final long DEFAULT_MAX_ATTEMPTS = 3; + public static final long DEFAULT_MAX_RETRIES = 3; /** * The default {@linkplain #delay(Duration) delay}: {@value} ms. @@ -147,7 +156,7 @@ public interface RetryPolicy { private @Nullable BackOff backOff; - private @Nullable Long maxAttempts; + private @Nullable Long maxRetries; private @Nullable Duration delay; @@ -174,7 +183,7 @@ public interface RetryPolicy { *

The supplied value will override any previously configured value. *

WARNING: If you configure a custom {@code BackOff} * strategy, you should not configure any of the following: - * {@link #maxAttempts(long) maxAttempts}, {@link #delay(Duration) delay}, + * {@link #maxRetries(long) maxRetries}, {@link #delay(Duration) delay}, * {@link #jitter(Duration) jitter}, {@link #multiplier(double) multiplier}, * or {@link #maxDelay(Duration) maxDelay}. * @param backOff the {@code BackOff} strategy @@ -188,17 +197,20 @@ public interface RetryPolicy { /** * Specify the maximum number of retry attempts. - *

The default is {@value #DEFAULT_MAX_ATTEMPTS}. + *

Note that {@code total attempts = 1 initial attempt + maxRetries attempts}. + * Thus, if {@code maxRetries} is set to 4, a retryable operation will be + * invoked at least once and at most 5 times. + *

The default is {@value #DEFAULT_MAX_RETRIES}. *

The supplied value will override any previously configured value. *

You should not specify this configuration option if you have * configured a custom {@link #backOff(BackOff) BackOff} strategy. - * @param maxAttempts the maximum number of retry attempts; + * @param maxRetries the maximum number of retry attempts; * must be positive (or zero for no retry) * @return this {@code Builder} instance for chained method invocations */ - public Builder maxAttempts(long maxAttempts) { - assertMaxAttemptsIsNotNegative(maxAttempts); - this.maxAttempts = maxAttempts; + public Builder maxRetries(long maxRetries) { + assertMaxRetriesIsNotNegative(maxRetries); + this.maxRetries = maxRetries; return this; } @@ -412,15 +424,15 @@ public interface RetryPolicy { public RetryPolicy build() { BackOff backOff = this.backOff; if (backOff != null) { - boolean misconfigured = (this.maxAttempts != null || this.delay != null || this.jitter != null || + boolean misconfigured = (this.maxRetries != null || this.delay != null || this.jitter != null || this.multiplier != null || this.maxDelay != null); Assert.state(!misconfigured, """ The following configuration options are not supported with a custom BackOff strategy: \ - maxAttempts, delay, jitter, multiplier, or maxDelay."""); + maxRetries, delay, jitter, multiplier, or maxDelay."""); } else { ExponentialBackOff exponentialBackOff = new ExponentialBackOff(); - exponentialBackOff.setMaxAttempts(this.maxAttempts != null ? this.maxAttempts : DEFAULT_MAX_ATTEMPTS); + exponentialBackOff.setMaxAttempts(this.maxRetries != null ? this.maxRetries : DEFAULT_MAX_RETRIES); exponentialBackOff.setInitialInterval(this.delay != null ? this.delay.toMillis() : DEFAULT_DELAY); exponentialBackOff.setMaxInterval(this.maxDelay != null ? this.maxDelay.toMillis() : DEFAULT_MAX_DELAY); exponentialBackOff.setMultiplier(this.multiplier != null ? this.multiplier : DEFAULT_MULTIPLIER); diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java index 437e8456e07..0974419f81b 100644 --- a/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java @@ -31,8 +31,8 @@ import org.springframework.util.backoff.BackOffExecution; * A basic implementation of {@link RetryOperations} that executes and potentially * retries a {@link Retryable} operation based on a configured {@link RetryPolicy}. * - *

By default, a retryable operation will be retried at most 3 times with a - * fixed backoff of 1 second. + *

By default, a retryable operation will be executed once and potentially + * retried at most 3 times with a fixed backoff of 1 second. * *

A {@link RetryListener} can be {@linkplain #setRetryListener(RetryListener) * registered} to react to events published during key retry phases (before a @@ -83,7 +83,7 @@ public class RetryTemplate implements RetryOperations { *

Defaults to {@code RetryPolicy.withDefaults()}. * @param retryPolicy the retry policy to use * @see RetryPolicy#withDefaults() - * @see RetryPolicy#withMaxAttempts(long) + * @see RetryPolicy#withMaxRetries(long) * @see RetryPolicy#builder() */ public void setRetryPolicy(RetryPolicy retryPolicy) { diff --git a/spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsRetryPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/MaxRetriesRetryPolicyTests.java similarity index 91% rename from spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsRetryPolicyTests.java rename to spring-core/src/test/java/org/springframework/core/retry/MaxRetriesRetryPolicyTests.java index 202e3849492..8bf7982d607 100644 --- a/spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsRetryPolicyTests.java +++ b/spring-core/src/test/java/org/springframework/core/retry/MaxRetriesRetryPolicyTests.java @@ -29,17 +29,17 @@ import static org.springframework.core.retry.RetryPolicy.Builder.DEFAULT_DELAY; import static org.springframework.util.backoff.BackOffExecution.STOP; /** - * Max attempts {@link RetryPolicy} tests. + * Max retries {@link RetryPolicy} tests. * * @author Mahmoud Ben Hassine * @author Sam Brannen * @since 7.0 */ -class MaxAttemptsRetryPolicyTests { +class MaxRetriesRetryPolicyTests { @Test - void maxAttempts() { - var retryPolicy = RetryPolicy.builder().maxAttempts(2).delay(Duration.ZERO).build(); + void maxRetries() { + var retryPolicy = RetryPolicy.builder().maxRetries(2).delay(Duration.ZERO).build(); var backOffExecution = retryPolicy.getBackOff().start(); var throwable = mock(Throwable.class); @@ -55,8 +55,8 @@ class MaxAttemptsRetryPolicyTests { } @Test - void maxAttemptsZero() { - var retryPolicy = RetryPolicy.builder().maxAttempts(0).delay(Duration.ZERO).build(); + void maxRetriesZero() { + var retryPolicy = RetryPolicy.builder().maxRetries(0).delay(Duration.ZERO).build(); var backOffExecution = retryPolicy.getBackOff().start(); var throwable = mock(Throwable.class); @@ -67,9 +67,9 @@ class MaxAttemptsRetryPolicyTests { } @Test - void maxAttemptsAndPredicate() { + void maxRetriesAndPredicate() { var retryPolicy = RetryPolicy.builder() - .maxAttempts(4) + .maxRetries(4) .delay(Duration.ofMillis(1)) .predicate(NumberFormatException.class::isInstance) .build(); @@ -94,9 +94,9 @@ class MaxAttemptsRetryPolicyTests { } @Test - void maxAttemptsWithIncludesAndExcludes() { + void maxRetriesWithIncludesAndExcludes() { var retryPolicy = RetryPolicy.builder() - .maxAttempts(6) + .maxRetries(6) .includes(RuntimeException.class, IOException.class) .excludes(FileNotFoundException.class, CustomFileSystemException.class) .build(); diff --git a/spring-core/src/test/java/org/springframework/core/retry/RetryPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/RetryPolicyTests.java index fac425dbf33..1aeaa303c43 100644 --- a/spring-core/src/test/java/org/springframework/core/retry/RetryPolicyTests.java +++ b/spring-core/src/test/java/org/springframework/core/retry/RetryPolicyTests.java @@ -64,15 +64,15 @@ class RetryPolicyTests { } @Test - void withMaxAttemptsPreconditions() { + void withMaxRetriesPreconditions() { assertThatIllegalArgumentException() - .isThrownBy(() -> RetryPolicy.withMaxAttempts(-1)) - .withMessageStartingWith("Invalid maxAttempts (-1)"); + .isThrownBy(() -> RetryPolicy.withMaxRetries(-1)) + .withMessageStartingWith("Invalid maxRetries (-1)"); } @Test - void withMaxAttempts() { - var policy = RetryPolicy.withMaxAttempts(5); + void withMaxRetries() { + var policy = RetryPolicy.withMaxRetries(5); assertThat(policy.shouldRetry(new AssertionError())).isTrue(); assertThat(policy.shouldRetry(new IOException())).isTrue(); @@ -96,7 +96,7 @@ class RetryPolicyTests { .isThrownBy(() -> RetryPolicy.builder().backOff(mock()).delay(Duration.ofMillis(10)).build()) .withMessage(""" The following configuration options are not supported with a custom BackOff strategy: \ - maxAttempts, delay, jitter, multiplier, or maxDelay."""); + maxRetries, delay, jitter, multiplier, or maxDelay."""); } @Test @@ -111,15 +111,15 @@ class RetryPolicyTests { } @Test - void maxAttemptsPreconditions() { + void maxRetriesPreconditions() { assertThatIllegalArgumentException() - .isThrownBy(() -> RetryPolicy.builder().maxAttempts(-1)) - .withMessageStartingWith("Invalid maxAttempts (-1)"); + .isThrownBy(() -> RetryPolicy.builder().maxRetries(-1)) + .withMessageStartingWith("Invalid maxRetries (-1)"); } @Test - void maxAttempts() { - var policy = RetryPolicy.builder().maxAttempts(5).build(); + void maxRetries() { + var policy = RetryPolicy.builder().maxRetries(5).build(); assertThat(policy.getBackOff()) .asInstanceOf(type(ExponentialBackOff.class)) diff --git a/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java index 2194dd72ac3..ae14b4fad7d 100644 --- a/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java +++ b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java @@ -57,7 +57,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; */ class RetryTemplateTests { - private final RetryPolicy retryPolicy = RetryPolicy.builder().maxAttempts(3).delay(Duration.ZERO).build(); + private final RetryPolicy retryPolicy = RetryPolicy.builder().maxRetries(3).delay(Duration.ZERO).build(); private final RetryTemplate retryTemplate = new RetryTemplate(retryPolicy); @@ -116,7 +116,7 @@ class RetryTemplateTests { @Test void retryWithInitialFailureAndZeroRetriesFixedBackOffPolicy() { - RetryPolicy retryPolicy = RetryPolicy.withMaxAttempts(0); + RetryPolicy retryPolicy = RetryPolicy.withMaxRetries(0); RetryTemplate retryTemplate = new RetryTemplate(retryPolicy); retryTemplate.setRetryListener(retryListener); @@ -138,7 +138,7 @@ class RetryTemplateTests { @Test void retryWithInitialFailureAndZeroRetriesBackOffPolicyFromBuilder() { - RetryPolicy retryPolicy = RetryPolicy.builder().maxAttempts(0).build(); + RetryPolicy retryPolicy = RetryPolicy.builder().maxRetries(0).build(); RetryTemplate retryTemplate = new RetryTemplate(retryPolicy); retryTemplate.setRetryListener(retryListener); @@ -263,7 +263,7 @@ class RetryTemplateTests { }; var retryPolicy = RetryPolicy.builder() - .maxAttempts(5) + .maxRetries(5) .delay(Duration.ofMillis(1)) .predicate(NumberFormatException.class::isInstance) .predicate(t -> t.getMessage().equals("Boom!")) @@ -311,7 +311,7 @@ class RetryTemplateTests { }; var retryPolicy = RetryPolicy.builder() - .maxAttempts(Integer.MAX_VALUE) + .maxRetries(Integer.MAX_VALUE) .delay(Duration.ZERO) .includes(IOException.class) .build(); @@ -344,13 +344,13 @@ class RetryTemplateTests { static final List includesAndExcludesRetryPolicies = List.of( argumentSet("Excludes", RetryPolicy.builder() - .maxAttempts(Integer.MAX_VALUE) + .maxRetries(Integer.MAX_VALUE) .delay(Duration.ZERO) .excludes(FileNotFoundException.class) .build()), argumentSet("Includes & Excludes", RetryPolicy.builder() - .maxAttempts(Integer.MAX_VALUE) + .maxRetries(Integer.MAX_VALUE) .delay(Duration.ZERO) .includes(IOException.class) .excludes(FileNotFoundException.class)