Skip to content

Commit 15dd320

Browse files
committed
Consistent maxAttempts (long) and delay/maxDelay (Duration) declarations
Includes timeUnit attribute in @retryable (aligned with @scheduled). See gh-34529 See gh-35110
1 parent bcdf26d commit 15dd320

File tree

11 files changed

+90
-82
lines changed

11 files changed

+90
-82
lines changed

spring-aop/src/main/java/org/springframework/aop/retry/AbstractRetryInterceptor.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.aop.retry;
1818

1919
import java.lang.reflect.Method;
20-
import java.time.Duration;
2120

2221
import org.aopalliance.intercept.MethodInterceptor;
2322
import org.aopalliance.intercept.MethodInvocation;
@@ -93,10 +92,10 @@ public AbstractRetryInterceptor() {
9392
.excludes(spec.excludes())
9493
.predicate(spec.predicate().forMethod(method))
9594
.maxAttempts(spec.maxAttempts())
96-
.delay(Duration.ofMillis(spec.delay()))
97-
.maxDelay(Duration.ofMillis(spec.maxDelay()))
98-
.jitter(Duration.ofMillis(spec.jitter()))
95+
.delay(spec.delay())
96+
.jitter(spec.jitter())
9997
.multiplier(spec.multiplier())
98+
.maxDelay(spec.maxDelay())
10099
.build();
101100
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
102101

@@ -136,10 +135,10 @@ public static Object adaptReactiveResult(
136135
Object result, ReactiveAdapter adapter, MethodRetrySpec spec, Method method) {
137136

138137
Publisher<?> publisher = adapter.toPublisher(result);
139-
Retry retry = Retry.backoff(spec.maxAttempts(), Duration.ofMillis(spec.delay()))
140-
.jitter((double) spec.jitter() / spec.delay())
138+
Retry retry = Retry.backoff(spec.maxAttempts(), spec.delay())
139+
.jitter((double) spec.jitter().toMillis() / spec.delay().toMillis())
141140
.multiplier(spec.multiplier())
142-
.maxBackoff(Duration.ofMillis(spec.maxDelay()))
141+
.maxBackoff(spec.maxDelay())
143142
.filter(spec.combinedPredicate().forMethod(method));
144143
publisher = (adapter.isMultiValue() ? Flux.from(publisher).retryWhen(retry) :
145144
Mono.from(publisher).retryWhen(retry));

spring-aop/src/main/java/org/springframework/aop/retry/MethodRetrySpec.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.aop.retry;
1818

19+
import java.time.Duration;
1920
import java.util.Collection;
2021
import java.util.Collections;
2122

@@ -42,18 +43,18 @@ public record MethodRetrySpec(
4243
Collection<Class<? extends Throwable>> includes,
4344
Collection<Class<? extends Throwable>> excludes,
4445
MethodRetryPredicate predicate,
45-
int maxAttempts,
46-
long delay,
47-
long jitter,
46+
long maxAttempts,
47+
Duration delay,
48+
Duration jitter,
4849
double multiplier,
49-
long maxDelay) {
50+
Duration maxDelay) {
5051

51-
public MethodRetrySpec(MethodRetryPredicate predicate, int maxAttempts, long delay) {
52-
this(predicate, maxAttempts, delay, 0, 1.0, Integer.MAX_VALUE);
52+
public MethodRetrySpec(MethodRetryPredicate predicate, long maxAttempts, Duration delay) {
53+
this(predicate, maxAttempts, delay, Duration.ofMillis(0), 1.0, Duration.ofMillis(Long.MAX_VALUE));
5354
}
5455

55-
public MethodRetrySpec(MethodRetryPredicate predicate, int maxAttempts, long delay,
56-
long jitter, double multiplier, long maxDelay) {
56+
public MethodRetrySpec(MethodRetryPredicate predicate, long maxAttempts, Duration delay,
57+
Duration jitter, double multiplier, Duration maxDelay) {
5758

5859
this(Collections.emptyList(), Collections.emptyList(), predicate, maxAttempts, delay,
5960
jitter, multiplier, maxDelay);

spring-aop/src/main/java/org/springframework/aop/retry/annotation/RetryAnnotationInterceptor.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package org.springframework.aop.retry.annotation;
1818

1919
import java.lang.reflect.Method;
20+
import java.time.Duration;
2021
import java.util.Arrays;
2122
import java.util.Map;
2223
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.concurrent.TimeUnit;
2325

2426
import org.jspecify.annotations.Nullable;
2527

@@ -57,17 +59,19 @@ public class RetryAnnotationInterceptor extends AbstractRetryInterceptor {
5759
}
5860
}
5961

62+
TimeUnit timeUnit = retryable.timeUnit();
6063
retrySpec = new MethodRetrySpec(
6164
Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()),
6265
instantiatePredicate(retryable.predicate()), retryable.maxAttempts(),
63-
retryable.delay(), retryable.jitter(),
64-
retryable.multiplier(), retryable.maxDelay());
66+
toDuration(retryable.delay(), timeUnit), toDuration(retryable.jitter(), timeUnit),
67+
retryable.multiplier(), toDuration(retryable.maxDelay(), timeUnit));
6568

6669
MethodRetrySpec existing = this.retrySpecCache.putIfAbsent(cacheKey, retrySpec);
6770
return (existing != null ? existing : retrySpec);
6871
}
6972

70-
private MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPredicate> predicateClass) {
73+
74+
private static MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPredicate> predicateClass) {
7175
if (predicateClass == MethodRetryPredicate.class) {
7276
return (method, throwable) -> true;
7377
}
@@ -79,4 +83,14 @@ private MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPre
7983
}
8084
}
8185

86+
private static Duration toDuration(long value, TimeUnit timeUnit) {
87+
try {
88+
return Duration.of(value, timeUnit.toChronoUnit());
89+
}
90+
catch (Exception ex) {
91+
throw new IllegalArgumentException(
92+
"Unsupported unit " + timeUnit + " for value \"" + value + "\": " + ex.getMessage());
93+
}
94+
}
95+
8296
}

spring-aop/src/main/java/org/springframework/aop/retry/annotation/Retryable.java

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.lang.annotation.Retention;
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
24+
import java.util.concurrent.TimeUnit;
2425

2526
import org.springframework.aop.retry.MethodRetryPredicate;
2627
import org.springframework.aot.hint.annotation.Reflective;
@@ -91,11 +92,13 @@
9192
* The maximum number of retry attempts, in addition to the initial invocation.
9293
* <p>The default is 3.
9394
*/
94-
int maxAttempts() default 3;
95+
long maxAttempts() default 3;
9596

9697
/**
97-
* The base delay after the initial invocation in milliseconds.
98-
* If a multiplier is specified, this serves as the initial delay to multiply from.
98+
* The base delay after the initial invocation in milliseconds. If a multiplier
99+
* is specified, this serves as the initial delay to multiply from.
100+
* <p>The time unit is milliseconds by default but can be overridden via
101+
* {@link #timeUnit}.
99102
* <p>The default is 1000.
100103
* @see #jitter()
101104
* @see #multiplier()
@@ -104,11 +107,13 @@
104107
long delay() default 1000;
105108

106109
/**
107-
* A jitter value (in milliseconds) for the base retry attempt, randomly
108-
* subtracted or added to the calculated delay, resulting in a value between
109-
* {@code delay - jitter} and {@code delay + jitter} but never below the base
110-
* {@link #delay()} or above {@link #maxDelay()}.
111-
* <p>If a multiplier is specified, it is applied to the jitter value as well.
110+
* A jitter value for the base retry attempt, randomly subtracted or added to
111+
* the calculated delay, resulting in a value between {@code delay - jitter}
112+
* and {@code delay + jitter} but never below the base {@link #delay()} or
113+
* above {@link #maxDelay()}. If a multiplier is specified, it is applied
114+
* to the jitter value as well.
115+
* <p>The time unit is milliseconds by default but can be overridden via
116+
* {@link #timeUnit}.
112117
* <p>The default is 0 (no jitter).
113118
* @see #delay()
114119
* @see #multiplier()
@@ -128,14 +133,22 @@
128133
double multiplier() default 1.0;
129134

130135
/**
131-
* The maximum delay for any retry attempt (in milliseconds), limiting
132-
* how far {@link #jitter()} and {@link #multiplier()} can increase the
133-
* {@linkplain #delay() delay}.
136+
* The maximum delay for any retry attempt, limiting how far {@link #jitter()}
137+
* and {@link #multiplier()} can increase the {@linkplain #delay() delay}.
138+
* <p>The time unit is milliseconds by default but can be overridden via
139+
* {@link #timeUnit}.
134140
* <p>The default is unlimited.
135141
* @see #delay()
136142
* @see #jitter()
137143
* @see #multiplier()
138144
*/
139-
long maxDelay() default Integer.MAX_VALUE;
145+
long maxDelay() default Long.MAX_VALUE;
146+
147+
/**
148+
* The {@link TimeUnit} to use for {@link #delay}, {@link #jitter},
149+
* and {@link #maxDelay}.
150+
* <p>Defaults to {@link TimeUnit#MILLISECONDS}.
151+
*/
152+
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
140153

141154
}

spring-aop/src/test/java/org/springframework/aop/retry/ReactiveRetryInterceptorTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.lang.reflect.Method;
2121
import java.nio.file.AccessDeniedException;
22+
import java.time.Duration;
2223
import java.util.concurrent.atomic.AtomicInteger;
2324

2425
import org.junit.jupiter.api.Test;
@@ -48,7 +49,8 @@ void withSimpleInterceptor() {
4849
NonAnnotatedBean target = new NonAnnotatedBean();
4950
ProxyFactory pf = new ProxyFactory();
5051
pf.setTarget(target);
51-
pf.addAdvice(new SimpleRetryInterceptor(new MethodRetrySpec((m, t) -> true, 5, 10)));
52+
pf.addAdvice(new SimpleRetryInterceptor(
53+
new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10))));
5254
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy();
5355

5456
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block())

spring-aop/src/test/java/org/springframework/aop/retry/RetryInterceptorTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.lang.reflect.Method;
2121
import java.nio.file.AccessDeniedException;
22+
import java.time.Duration;
2223

2324
import org.junit.jupiter.api.Test;
2425

@@ -44,7 +45,8 @@ void withSimpleInterceptor() {
4445
NonAnnotatedBean target = new NonAnnotatedBean();
4546
ProxyFactory pf = new ProxyFactory();
4647
pf.setTarget(target);
47-
pf.addAdvice(new SimpleRetryInterceptor(new MethodRetrySpec((m, t) -> true, 5, 10)));
48+
pf.addAdvice(new SimpleRetryInterceptor(
49+
new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10))));
4850
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy();
4951

5052
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");

spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
*
3636
* <p>Also provides factory methods and a fluent builder API for creating retry
3737
* policies with common configurations. See {@link #withDefaults()},
38-
* {@link #withMaxAttempts(int)}, {@link #withMaxElapsedTime(Duration)},
38+
* {@link #withMaxAttempts(long)}, {@link #withMaxElapsedTime(Duration)},
3939
* {@link #builder()}, and the configuration options in {@link Builder} for details.
4040
*
4141
* @author Sam Brannen
@@ -55,7 +55,6 @@ public interface RetryPolicy {
5555
*/
5656
boolean shouldRetry(Throwable throwable);
5757

58-
5958
/**
6059
* Get the {@link BackOff} strategy to use for this retry policy.
6160
* <p>Defaults to a fixed backoff of {@value Builder#DEFAULT_DELAY} milliseconds
@@ -84,10 +83,10 @@ static RetryPolicy withDefaults() {
8483
* <p>The returned policy uses a fixed backoff of {@value Builder#DEFAULT_DELAY}
8584
* milliseconds.
8685
* @param maxAttempts the maximum number of retry attempts; must be greater than zero
87-
* @see Builder#maxAttempts(int)
86+
* @see Builder#maxAttempts(long)
8887
* @see FixedBackOff
8988
*/
90-
static RetryPolicy withMaxAttempts(int maxAttempts) {
89+
static RetryPolicy withMaxAttempts(long maxAttempts) {
9190
Assert.isTrue(maxAttempts > 0, "Max attempts must be greater than zero");
9291
return builder().backOff(new FixedBackOff(Builder.DEFAULT_DELAY, maxAttempts)).build();
9392
}
@@ -120,9 +119,9 @@ static Builder builder() {
120119
final class Builder {
121120

122121
/**
123-
* The default {@linkplain #maxAttempts(int) max attempts}: {@value}.
122+
* The default {@linkplain #maxAttempts(long) max attempts}: {@value}.
124123
*/
125-
public static final int DEFAULT_MAX_ATTEMPTS = 3;
124+
public static final long DEFAULT_MAX_ATTEMPTS = 3;
126125

127126
/**
128127
* The default {@linkplain #delay(Duration) delay}: {@value} ms.
@@ -143,7 +142,7 @@ final class Builder {
143142

144143
private @Nullable BackOff backOff;
145144

146-
private int maxAttempts;
145+
private long maxAttempts;
147146

148147
private @Nullable Duration delay;
149148

@@ -172,7 +171,7 @@ private Builder() {
172171
* <p>The supplied value will override any previously configured value.
173172
* <p><strong>WARNING</strong>: If you configure a custom {@code BackOff}
174173
* strategy, you should not configure any of the following:
175-
* {@link #maxAttempts(int) maxAttempts}, {@link #delay(Duration) delay},
174+
* {@link #maxAttempts(long) maxAttempts}, {@link #delay(Duration) delay},
176175
* {@link #jitter(Duration) jitter}, {@link #multiplier(double) multiplier},
177176
* {@link #maxDelay(Duration) maxDelay}, or {@link #maxElapsedTime(Duration)
178177
* maxElapsedTime}.
@@ -195,7 +194,7 @@ public Builder backOff(BackOff backOff) {
195194
* greater than zero
196195
* @return this {@code Builder} instance for chained method invocations
197196
*/
198-
public Builder maxAttempts(int maxAttempts) {
197+
public Builder maxAttempts(long maxAttempts) {
199198
Assert.isTrue(maxAttempts > 0, "Max attempts must be greater than zero");
200199
this.maxAttempts = maxAttempts;
201200
return this;
@@ -285,7 +284,7 @@ public Builder multiplier(double multiplier) {
285284
* @see #multiplier(double)
286285
*/
287286
public Builder maxDelay(Duration maxDelay) {
288-
assertIsPositive("max delay", maxDelay);
287+
assertIsPositive("maxDelay", maxDelay);
289288
this.maxDelay = maxDelay;
290289
return this;
291290
}
@@ -300,7 +299,7 @@ public Builder maxDelay(Duration maxDelay) {
300299
* @return this {@code Builder} instance for chained method invocations
301300
*/
302301
public Builder maxElapsedTime(Duration maxElapsedTime) {
303-
assertIsPositive("max elapsed time", maxElapsedTime);
302+
assertIsPositive("maxElapsedTime", maxElapsedTime);
304303
this.maxElapsedTime = maxElapsedTime;
305304
return this;
306305
}

spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ public class RetryTemplate implements RetryOperations {
5555

5656
private static final LogAccessor logger = new LogAccessor(RetryTemplate.class);
5757

58-
5958
private RetryPolicy retryPolicy = RetryPolicy.withDefaults();
6059

6160
private RetryListener retryListener = new RetryListener() {};
@@ -84,7 +83,7 @@ public RetryTemplate(RetryPolicy retryPolicy) {
8483
* <p>Defaults to {@code RetryPolicy.withDefaults()}.
8584
* @param retryPolicy the retry policy to use
8685
* @see RetryPolicy#withDefaults()
87-
* @see RetryPolicy#withMaxAttempts(int)
86+
* @see RetryPolicy#withMaxAttempts(long)
8887
* @see RetryPolicy#withMaxElapsedTime(Duration)
8988
* @see RetryPolicy#builder()
9089
*/
@@ -105,6 +104,7 @@ public void setRetryListener(RetryListener retryListener) {
105104
this.retryListener = retryListener;
106105
}
107106

107+
108108
/**
109109
* Execute the supplied {@link Retryable} according to the configured retry
110110
* and backoff policies.

spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public class ExponentialBackOff implements BackOff {
103103

104104
private long maxElapsedTime = DEFAULT_MAX_ELAPSED_TIME;
105105

106-
private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
106+
private long maxAttempts = DEFAULT_MAX_ATTEMPTS;
107107

108108

109109
/**
@@ -232,7 +232,7 @@ public long getMaxElapsedTime() {
232232
* @since 6.1
233233
* @see #setMaxElapsedTime
234234
*/
235-
public void setMaxAttempts(int maxAttempts) {
235+
public void setMaxAttempts(long maxAttempts) {
236236
this.maxAttempts = maxAttempts;
237237
}
238238

@@ -243,7 +243,7 @@ public void setMaxAttempts(int maxAttempts) {
243243
* @since 6.1
244244
* @see #getMaxElapsedTime()
245245
*/
246-
public int getMaxAttempts() {
246+
public long getMaxAttempts() {
247247
return this.maxAttempts;
248248
}
249249

0 commit comments

Comments
 (0)