Feature request / Enhancement
Background
When configuring maximum retry attempts in Spring Framework (RetryPolicy, RetryTemplate), the maxAttempts parameter is currently documented and implemented as the "total number of executions (including the first attempt)". This is supposed to match the definition in spring-retry, but the way it is explained in spring-retry is more direct and user-friendly. Furthermore, in recent @spring-projects/spring-framework tests, its behavior appears inconsistent with the documentation and with spring-retry itself.
How it works in spring-retry
"Includes the initial attempt before the retries begin so, generally, will be >= 1. For example setting this property to 3 means 3 attempts total (initial + 2 retries)."
Javadoc and parameter descriptions for maxAttempts in spring-retry make it clear: setting to 3 means 3 total attempts (1 initial + 2 retries). This is incredibly clear for users ('don't make me think').
How it's defined and tested in spring-framework
Documentation Reference: The docs in Spring Framework 7.0 clearly state that the initial attempt plus maxAttempts - 1 retries is the intended behavior:
framework-docs/modules/ROOT/pages/core/resilience.adoc
- spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java
- spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java
Recent tests (see link above) indicate the initial call is not counted towards maxAttempts—meaning that if you set maxAttempts = 3, you may get the initial attempt + 3 more as retries (total of 4). This is not consistent with the spring-retry definition and can lead to off-by-one confusion for users.
Proposal
- Goal: Unify semantics so that the initial attempt IS INCLUDED in
maxAttemptsfor both modules—so a value of N means N total tries (the initial attempt + N-1 retries). - Update docs, javadocs, examples, and definition so that it matches the more user-friendly approach of
spring-retry. - Review and (if needed) update tests and implementation to avoid confusion and ensure the initial call is counted as one of the
maxAttempts. - Add code comments and examples highlighting this for future contributors.
References
- Spring Retry:
MaxAttemptsRetryPolicy,SimpleRetryPolicy - Spring Framework:
RetryPolicy.java,RetryTemplateTests.java
If possible, please harmonize the documentation, implementation, and user experience between Spring Framework retry and spring-retry. The initial attempt should count towards maxAttempts (as in spring-retry), making it easier for users and developers to reason about behavior.
Comment From: sbrannen
Hi @dol,
Congratulations on submitting your first issue for the Spring Framework. 👍
I realize there can be some confusion between "max attempts" and "max retry attempts", but before we proceed I'd like to ask a few questions.
When configuring maximum retry attempts in Spring Framework (
RetryPolicy,RetryTemplate), themaxAttemptsparameter is currently documented and implemented as the "total number of executions (including the first attempt)".
I cannot seem to find any text like that.
Can you please provide a link to that documentation?
Furthermore, in recent
@spring-projects/spring-frameworktests, its behavior appears inconsistent with the documentation
Can you please point to a specific test which demonstrates that?
How it's defined and tested in spring-framework
Documentation Reference: The docs in Spring Framework 7.0 clearly state that the initial attempt plus maxAttempts - 1 retries is the intended behavior:
I also cannot find any documentation which states that.
Can you please expound on what led you to interpret the documentation like that?
Recent tests (see link above) indicate the initial call is not counted towards maxAttempts—meaning that if you set
maxAttempts = 3, you may get the initial attempt + 3 more as retries (total of 4).
That is correct.
This is not consistent with the
spring-retrydefinition and can lead to off-by-one confusion for users.
That is also correct.
The behavior in Spring Retry and Core Retry (in the Spring Framework) differ in that regard.
- Spring Retry: talks about max "attempts"
- Core Retry: talks about max "retry attempts" (even though the configuration is named
maxAttempts)
As far as I know, the documentation in Spring Framework is consistent in that regard. Although, the wording may not be as clear as it could be.
The initial attempt should count towards
maxAttempts(as in spring-retry), making it easier for users and developers to reason about behavior.
Again, I understand the confusion between "max attempts" and "max retry attempts", so we'll discuss this again within the team.
In the interim, however, I would appreciate it if you could answer the questions I raised above.
Thanks
Comment From: jhoeller
Note that in terms of semantic alignment, we are not primarily targeting Spring Retry (or Resilience4J for that matter) but rather the MicroProfile @Retry annotation, the Micronaut @Retryable annotation, and Reactor's RetryBackoffSpec (which we directly integrate with for reactive methods annotated with Spring's @Retryable). All of the latter interpret the specified number of attempts as retry attempts in addition to the initial invocation, and the common annotation default is a maximum of 3 such retry attempts.
FWIW, both specifying the number of retry attempts as well as the default of 3 such attempts in addition to the original call is common on other platforms as well, such as on .NET (https://www.pollydocs.org/strategies/retry.html). Arguably this is common industry consensus for similar programming model features that we are aligning with here, or at least common enough in the industry for us to adopt that approach as well.
That said, we could offer both variants side by side, for example as retryAttempts and totalAttempts attributes (with only one of them to be set) on the annotation. Even then, the remaining question is whether the default would be 3 retry attempts or 3 total attempts. In some way, this would make the annotation harder to understand, so I'm not sure this would actually be helpful.
Comment From: sbrannen
Another proposal...
We could rename maxAttempts to maxRetries in both the @Retryable annotation and RetryPolicy builder, and the static factory method for the policy would be RetryPolicy.withMaxRetries().
By doing that, we avoid any confusion regarding which "attempts" we are referring to.