diff --git a/framework-docs/modules/ROOT/pages/core/resilience.adoc b/framework-docs/modules/ROOT/pages/core/resilience.adoc index a186c9c81ff..fa68ac4fd43 100644 --- a/framework-docs/modules/ROOT/pages/core/resilience.adoc +++ b/framework-docs/modules/ROOT/pages/core/resilience.adoc @@ -81,6 +81,13 @@ public void sendNotification() { } ---- +[NOTE] +==== +When `delay` is `0` combined with a positive `jitter`, the delay never grows +regardless of any configured `multiplier`, so the full configured `jitter` is +applied directly as a random delay in the range from `0` to `min(jitter, maxDelay)`. +==== + Last but not least, `@Retryable` also works for reactive methods with a reactive return type, decorating the pipeline with Reactor's retry capabilities: @@ -263,6 +270,13 @@ and an exponential back-off strategy with a bit of jitter. () -> jmsClient.destination("notifications").send(...)); ---- +[NOTE] +==== +When `delay` is zero combined with a positive `jitter`, the delay never grows +regardless of any configured `multiplier`, so the full configured `jitter` is +applied directly as a random delay in the range from zero to `min(jitter, maxDelay)`. +==== + [TIP] ==== Although the factory methods and builder API for `RetryPolicy` cover most common 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 f86d69d6cc4..9f929febcdb 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 @@ -197,6 +197,10 @@ public @interface Retryable { * 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. + *

When {@link #delay()} is {@code 0} combined with a positive jitter, + * the delay never grows regardless of any configured multiplier, so the + * full configured jitter is applied directly as a random delay in the range + * from {@code 0} to {@code min(jitter, maxDelay)}. *

The time unit is milliseconds by default but can be overridden via * {@link #timeUnit}. *

The default is 0 (no jitter). 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 b664c7961f2..374e02dfb47 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 @@ -281,6 +281,11 @@ public interface RetryPolicy { * {@linkplain #maxDelay(Duration) max delay}. *

If a {@linkplain #multiplier(double) multiplier} is specified, it * is applied to the jitter value as well. + *

When the configured {@linkplain #delay(Duration) delay} is zero + * combined with a positive jitter, the delay never grows regardless of + * any configured multiplier, so the full configured jitter is applied + * directly as a random delay in the range from zero to + * {@code min(jitter, maxDelay)}. *

The default is no jitter. *

The supplied value will override any previously configured value. *

You should not specify this configuration option if you have diff --git a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java index fd48e023e15..e60484b8d16 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java @@ -154,6 +154,10 @@ public class ExponentialBackOff implements BackOff { * {@code initialInterval} or above {@code maxInterval}. *

If a {@code multiplier} is specified, it is applied to the jitter value * as well. + *

When {@code initialInterval} is {@code 0} combined with a positive + * jitter, the interval never grows regardless of any configured multiplier, + * so the full configured jitter is applied directly as a random interval in + * the range from {@code 0} to {@code min(jitter, maxInterval)}. * @param jitter the jitter value in milliseconds * @since 7.0 */ @@ -311,7 +315,9 @@ public class ExponentialBackOff implements BackOff { long jitter = getJitter(); if (jitter > 0) { long initialInterval = getInitialInterval(); - long applicableJitter = jitter * (interval / initialInterval); + // When initialInterval is 0 the interval never grows, so the scale factor + // stays at its baseline value of 1 and the full configured jitter is applied. + long applicableJitter = jitter * (initialInterval > 0 ? (interval / initialInterval) : 1); long min = Math.max(interval - applicableJitter, initialInterval); long max = Math.min(interval + applicableJitter, getMaxInterval()); return min + (long) (Math.random() * (max - min)); diff --git a/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java b/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java index 40271f5c744..50999afc589 100644 --- a/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java +++ b/spring-core/src/test/java/org/springframework/util/ExponentialBackOffTests.java @@ -27,6 +27,7 @@ import org.springframework.util.backoff.ExponentialBackOff; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * Tests for {@link ExponentialBackOff}. @@ -118,6 +119,18 @@ class ExponentialBackOffTests { assertThatIllegalArgumentException().isThrownBy(() -> backOff.setMultiplier(0.9)); } + @Test // gh-36932 + void jitterWithZeroInitialInterval() { + ExponentialBackOff backOff = new ExponentialBackOff(); + backOff.setInitialInterval(0); + backOff.setJitter(100); + BackOffExecution execution = backOff.start(); + + // 'initialInterval = 0' and 'jitter > 0' are both individually accepted + // configurations, so their combination must not throw. + assertThatNoException().isThrownBy(execution::nextBackOff); + } + @Test void maxIntervalReachedImmediately() { ExponentialBackOff backOff = new ExponentialBackOff(1000L, 2.0);