Consistent maxAttempts (long) and delay/maxDelay (Duration) declarations

Includes timeUnit attribute in @Retryable (aligned with @Scheduled).

See gh-34529
See gh-35110
This commit is contained in:
Juergen Hoeller
2025-06-28 11:40:00 +02:00
parent bcdf26d492
commit 15dd320b95
11 changed files with 90 additions and 82 deletions
@@ -17,7 +17,6 @@
package org.springframework.aop.retry;
import java.lang.reflect.Method;
import java.time.Duration;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
@@ -93,10 +92,10 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
.excludes(spec.excludes())
.predicate(spec.predicate().forMethod(method))
.maxAttempts(spec.maxAttempts())
.delay(Duration.ofMillis(spec.delay()))
.maxDelay(Duration.ofMillis(spec.maxDelay()))
.jitter(Duration.ofMillis(spec.jitter()))
.delay(spec.delay())
.jitter(spec.jitter())
.multiplier(spec.multiplier())
.maxDelay(spec.maxDelay())
.build();
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
@@ -136,10 +135,10 @@ 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(), Duration.ofMillis(spec.delay()))
.jitter((double) spec.jitter() / spec.delay())
Retry retry = Retry.backoff(spec.maxAttempts(), spec.delay())
.jitter((double) spec.jitter().toMillis() / spec.delay().toMillis())
.multiplier(spec.multiplier())
.maxBackoff(Duration.ofMillis(spec.maxDelay()))
.maxBackoff(spec.maxDelay())
.filter(spec.combinedPredicate().forMethod(method));
publisher = (adapter.isMultiValue() ? Flux.from(publisher).retryWhen(retry) :
Mono.from(publisher).retryWhen(retry));
@@ -16,6 +16,7 @@
package org.springframework.aop.retry;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
@@ -42,18 +43,18 @@ public record MethodRetrySpec(
Collection<Class<? extends Throwable>> includes,
Collection<Class<? extends Throwable>> excludes,
MethodRetryPredicate predicate,
int maxAttempts,
long delay,
long jitter,
long maxAttempts,
Duration delay,
Duration jitter,
double multiplier,
long maxDelay) {
Duration maxDelay) {
public MethodRetrySpec(MethodRetryPredicate predicate, int maxAttempts, long delay) {
this(predicate, maxAttempts, delay, 0, 1.0, Integer.MAX_VALUE);
public MethodRetrySpec(MethodRetryPredicate predicate, long maxAttempts, Duration delay) {
this(predicate, maxAttempts, delay, Duration.ofMillis(0), 1.0, Duration.ofMillis(Long.MAX_VALUE));
}
public MethodRetrySpec(MethodRetryPredicate predicate, int maxAttempts, long delay,
long jitter, double multiplier, long maxDelay) {
public MethodRetrySpec(MethodRetryPredicate predicate, long maxAttempts, Duration delay,
Duration jitter, double multiplier, Duration maxDelay) {
this(Collections.emptyList(), Collections.emptyList(), predicate, maxAttempts, delay,
jitter, multiplier, maxDelay);
@@ -17,9 +17,11 @@
package org.springframework.aop.retry.annotation;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.jspecify.annotations.Nullable;
@@ -57,17 +59,19 @@ public class RetryAnnotationInterceptor extends AbstractRetryInterceptor {
}
}
TimeUnit timeUnit = retryable.timeUnit();
retrySpec = new MethodRetrySpec(
Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()),
instantiatePredicate(retryable.predicate()), retryable.maxAttempts(),
retryable.delay(), retryable.jitter(),
retryable.multiplier(), retryable.maxDelay());
toDuration(retryable.delay(), timeUnit), toDuration(retryable.jitter(), timeUnit),
retryable.multiplier(), toDuration(retryable.maxDelay(), timeUnit));
MethodRetrySpec existing = this.retrySpecCache.putIfAbsent(cacheKey, retrySpec);
return (existing != null ? existing : retrySpec);
}
private MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPredicate> predicateClass) {
private static MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPredicate> predicateClass) {
if (predicateClass == MethodRetryPredicate.class) {
return (method, throwable) -> true;
}
@@ -79,4 +83,14 @@ public class RetryAnnotationInterceptor extends AbstractRetryInterceptor {
}
}
private static Duration toDuration(long value, TimeUnit timeUnit) {
try {
return Duration.of(value, timeUnit.toChronoUnit());
}
catch (Exception ex) {
throw new IllegalArgumentException(
"Unsupported unit " + timeUnit + " for value \"" + value + "\": " + ex.getMessage());
}
}
}
@@ -21,6 +21,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
import org.springframework.aop.retry.MethodRetryPredicate;
import org.springframework.aot.hint.annotation.Reflective;
@@ -91,11 +92,13 @@ public @interface Retryable {
* The maximum number of retry attempts, in addition to the initial invocation.
* <p>The default is 3.
*/
int maxAttempts() default 3;
long maxAttempts() default 3;
/**
* The base delay after the initial invocation in milliseconds.
* If a multiplier is specified, this serves as the initial delay to multiply from.
* The base delay after the initial invocation in milliseconds. If a multiplier
* is specified, this serves as the initial delay to multiply from.
* <p>The time unit is milliseconds by default but can be overridden via
* {@link #timeUnit}.
* <p>The default is 1000.
* @see #jitter()
* @see #multiplier()
@@ -104,11 +107,13 @@ public @interface Retryable {
long delay() default 1000;
/**
* A jitter value (in milliseconds) for the base retry attempt, randomly
* subtracted or added to the calculated delay, resulting in a value between
* {@code delay - jitter} and {@code delay + jitter} but never below the base
* {@link #delay()} or above {@link #maxDelay()}.
* <p>If a multiplier is specified, it is applied to the jitter value as well.
* A jitter value for the base retry attempt, randomly subtracted or added to
* the calculated delay, resulting in a value between {@code delay - jitter}
* and {@code delay + jitter} but never below the base {@link #delay()} or
* above {@link #maxDelay()}. If a multiplier is specified, it is applied
* to the jitter value as well.
* <p>The time unit is milliseconds by default but can be overridden via
* {@link #timeUnit}.
* <p>The default is 0 (no jitter).
* @see #delay()
* @see #multiplier()
@@ -128,14 +133,22 @@ public @interface Retryable {
double multiplier() default 1.0;
/**
* The maximum delay for any retry attempt (in milliseconds), limiting
* how far {@link #jitter()} and {@link #multiplier()} can increase the
* {@linkplain #delay() delay}.
* The maximum delay for any retry attempt, limiting how far {@link #jitter()}
* and {@link #multiplier()} can increase the {@linkplain #delay() delay}.
* <p>The time unit is milliseconds by default but can be overridden via
* {@link #timeUnit}.
* <p>The default is unlimited.
* @see #delay()
* @see #jitter()
* @see #multiplier()
*/
long maxDelay() default Integer.MAX_VALUE;
long maxDelay() default Long.MAX_VALUE;
/**
* The {@link TimeUnit} to use for {@link #delay}, {@link #jitter},
* and {@link #maxDelay}.
* <p>Defaults to {@link TimeUnit#MILLISECONDS}.
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}