From 6cba48e66e43d7443af9acad59e9697b870a1090 Mon Sep 17 00:00:00 2001 From: Andreas Ahlenstorf Date: Thu, 25 May 2023 13:53:58 +0200 Subject: [PATCH 1/2] Introduce java.time.Duration --- .../retry/support/RetryTemplateBuilder.java | 118 +++++++++++++++++- .../support/RetryTemplateBuilderTests.java | 95 +++++++++++++- 2 files changed, 208 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java b/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java index 701b131c..0b2b85f0 100644 --- a/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java +++ b/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java @@ -15,6 +15,7 @@ */ package org.springframework.retry.support; +import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -83,6 +84,7 @@ * @author Aleksandr Shamukov * @author Artem Bilan * @author Kim In Hoi + * @author Andreas Ahlenstorf * @since 1.3 */ public class RetryTemplateBuilder { @@ -122,16 +124,43 @@ public RetryTemplateBuilder maxAttempts(int maxAttempts) { * @param timeout whole execution timeout in milliseconds * @return this * @see TimeoutRetryPolicy + * @deprecated Use {@link #withTimeout(long)} instead. */ + @Deprecated(since = "2.0.2", forRemoval = true) public RetryTemplateBuilder withinMillis(long timeout) { - Assert.isTrue(timeout > 0, "Timeout should be positive"); + return withTimeout(timeout); + } + + /** + * Retry until {@code timeout} has passed since the initial attempt. + * @param timeoutMillis timeout in milliseconds + * @return this + * @see TimeoutRetryPolicy + * @throws IllegalArgumentException if timeout is {@literal <=} 0 or if another retry + * policy has already been configured. + * @since 2.0.2 + */ + public RetryTemplateBuilder withTimeout(long timeoutMillis) { + Assert.isTrue(timeoutMillis > 0, "timeoutMillis should be greater than 0"); Assert.isNull(this.baseRetryPolicy, "You have already selected another retry policy"); - TimeoutRetryPolicy timeoutRetryPolicy = new TimeoutRetryPolicy(); - timeoutRetryPolicy.setTimeout(timeout); - this.baseRetryPolicy = timeoutRetryPolicy; + this.baseRetryPolicy = new TimeoutRetryPolicy(timeoutMillis); return this; } + /** + * Retry until {@code timeout} has passed since the initial attempt. + * @param timeout duration for how long retries should be attempted + * @return this + * @see TimeoutRetryPolicy + * @throws IllegalArgumentException if timeout is {@code null} or 0, or if another + * retry policy has already been configured. + * @since 2.0.2 + */ + public RetryTemplateBuilder withTimeout(Duration timeout) { + Assert.notNull(timeout, "timeout is null"); + return withTimeout(timeout.toMillis()); + } + /** * Allows infinite retry, do not limit attempts by number or time. *

@@ -180,6 +209,27 @@ public RetryTemplateBuilder exponentialBackoff(long initialInterval, double mult return exponentialBackoff(initialInterval, multiplier, maxInterval, false); } + /** + * Use exponential backoff policy. The formula of backoff period: + *

+ * {@code currentInterval = Math.min(initialInterval * Math.pow(multiplier, retryNum), maxInterval)} + *

+ * (for first attempt retryNum = 0) + * @param initialInterval initial sleep duration + * @param multiplier backoff interval multiplier + * @param maxInterval maximum backoff duration + * @return this + * @see ExponentialBackOffPolicy + * @throws IllegalArgumentException if initialInterval is {@code null}, multiplier is + * {@literal <=} 1, or if maxInterval is {@code null} + * @since 2.0.2 + */ + public RetryTemplateBuilder exponentialBackoff(Duration initialInterval, double multiplier, Duration maxInterval) { + Assert.notNull(initialInterval, "initialInterval is null"); + Assert.notNull(maxInterval, "maxInterval is null"); + return exponentialBackoff(initialInterval.toMillis(), multiplier, maxInterval.toMillis(), false); + } + /** * Use exponential backoff policy. The formula of backoff period (without randomness): *

@@ -210,6 +260,31 @@ public RetryTemplateBuilder exponentialBackoff(long initialInterval, double mult return this; } + /** + * Use exponential backoff policy. The formula of backoff period (without randomness): + *

+ * {@code currentInterval = Math.min(initialInterval * Math.pow(multiplier, retryNum), maxInterval)} + *

+ * (for first attempt retryNum = 0) + * @param initialInterval initial sleep duration + * @param multiplier backoff interval multiplier + * @param maxInterval maximum backoff duration + * @param withRandom adds some randomness to backoff intervals. For details, see + * {@link ExponentialRandomBackOffPolicy} + * @return this + * @see ExponentialBackOffPolicy + * @see ExponentialRandomBackOffPolicy + * @throws IllegalArgumentException if initialInterval is {@code null}, multiplier is + * {@literal <=} 1, or maxInterval is {@code null} + * @since 2.0.2 + */ + public RetryTemplateBuilder exponentialBackoff(Duration initialInterval, double multiplier, Duration maxInterval, + boolean withRandom) { + Assert.notNull(initialInterval, "initialInterval is null"); + Assert.notNull(maxInterval, "maxInterval is null"); + return this.exponentialBackoff(initialInterval.toMillis(), multiplier, maxInterval.toMillis(), withRandom); + } + /** * Perform each retry after fixed amount of time. * @param interval fixed interval in milliseconds @@ -225,6 +300,24 @@ public RetryTemplateBuilder fixedBackoff(long interval) { return this; } + /** + * Perform each retry after fixed amount of time. + * @param interval fixed backoff duration + * @return this + * @see FixedBackOffPolicy + * @throws IllegalArgumentException if another backoff policy has already been + * configured, interval is {@code null} or less than 1 millisecond + * @since 2.0.2 + */ + public RetryTemplateBuilder fixedBackoff(Duration interval) { + Assert.notNull(interval, "interval is null"); + + long millis = interval.toMillis(); + Assert.isTrue(millis >= 1, "interval is less than 1 millisecond"); + + return this.fixedBackoff(millis); + } + /** * Use {@link UniformRandomBackOffPolicy}, see it's doc for details. * @param minInterval in milliseconds @@ -244,6 +337,23 @@ public RetryTemplateBuilder uniformRandomBackoff(long minInterval, long maxInter return this; } + /** + * Use {@link UniformRandomBackOffPolicy}. + * @param minInterval minimum backoff duration + * @param maxInterval maximum backoff duration + * @return this + * @see UniformRandomBackOffPolicy + * @throws IllegalArgumentException if minInterval is {@code null} or {@literal <} 1, + * maxInterval is {@code null} or {@literal <} 1, maxInterval {@literal >=} + * minInterval or if another backoff policy has already been configured. + * @since 2.0.2 + */ + public RetryTemplateBuilder uniformRandomBackoff(Duration minInterval, Duration maxInterval) { + Assert.notNull(minInterval, "minInterval is null"); + Assert.notNull(maxInterval, "maxInterval is null"); + return this.uniformRandomBackoff(minInterval.toMillis(), maxInterval.toMillis()); + } + /** * Do not pause between attempts, retry immediately. * @return this diff --git a/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java b/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java index 5427bfd4..58799ec4 100644 --- a/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java +++ b/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java @@ -18,6 +18,7 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -29,6 +30,7 @@ import org.springframework.retry.RetryPolicy; import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.backoff.ExponentialRandomBackOffPolicy; +import org.springframework.retry.backoff.FixedBackOffPolicy; import org.springframework.retry.backoff.NoBackOffPolicy; import org.springframework.retry.backoff.UniformRandomBackOffPolicy; import org.springframework.retry.policy.AlwaysRetryPolicy; @@ -53,6 +55,7 @@ * @author Aleksandr Shamukov * @author Kim In Hoi * @author Gary Russell + * @author Andreas Ahlenstorf */ public class RetryTemplateBuilderTests { @@ -113,10 +116,11 @@ public void testBasicCustomization() { @Test public void testFailOnRetryPoliciesConflict() { assertThatIllegalArgumentException() - .isThrownBy(() -> RetryTemplate.builder().maxAttempts(3).withinMillis(1000).build()); + .isThrownBy(() -> RetryTemplate.builder().maxAttempts(3).withTimeout(1000).build()); } @Test + @SuppressWarnings("removal") public void testTimeoutPolicy() { RetryTemplate template = RetryTemplate.builder().withinMillis(10000).build(); @@ -127,6 +131,28 @@ public void testTimeoutPolicy() { assertThat(((TimeoutRetryPolicy) policyTuple.baseRetryPolicy).getTimeout()).isEqualTo(10000); } + @Test + public void testTimeoutMillis() { + RetryTemplate template = RetryTemplate.builder().withTimeout(10000).build(); + + PolicyTuple policyTuple = PolicyTuple.extractWithAsserts(template); + assertDefaultClassifier(policyTuple); + + assertThat(policyTuple.baseRetryPolicy).isInstanceOf(TimeoutRetryPolicy.class); + assertThat(((TimeoutRetryPolicy) policyTuple.baseRetryPolicy).getTimeout()).isEqualTo(10000); + } + + @Test + public void testTimeoutDuration() { + RetryTemplate template = RetryTemplate.builder().withTimeout(Duration.ofSeconds(3)).build(); + + PolicyTuple policyTuple = PolicyTuple.extractWithAsserts(template); + assertDefaultClassifier(policyTuple); + + assertThat(policyTuple.baseRetryPolicy).isInstanceOf(TimeoutRetryPolicy.class); + assertThat(((TimeoutRetryPolicy) policyTuple.baseRetryPolicy).getTimeout()).isEqualTo(3000); + } + @Test public void testInfiniteRetry() { RetryTemplate template = RetryTemplate.builder().infiniteRetry().build(); @@ -190,24 +216,91 @@ public void testFailOnBackOffPolicyConflict() { .isThrownBy(() -> RetryTemplate.builder().noBackoff().fixedBackoff(1000).build()); } + @Test + public void testFixedBackoff() { + RetryTemplate template = RetryTemplate.builder().fixedBackoff(200).build(); + FixedBackOffPolicy policy = getPropertyValue(template, "backOffPolicy", FixedBackOffPolicy.class); + + assertThat(policy.getBackOffPeriod()).isEqualTo(200); + } + + @Test + public void testFixedBackoffDuration() { + RetryTemplate template = RetryTemplate.builder().fixedBackoff(Duration.ofSeconds(1)).build(); + FixedBackOffPolicy policy = getPropertyValue(template, "backOffPolicy", FixedBackOffPolicy.class); + + assertThat(policy.getBackOffPeriod()).isEqualTo(1000); + } + @Test public void testUniformRandomBackOff() { RetryTemplate template = RetryTemplate.builder().uniformRandomBackoff(10, 100).build(); assertThat(getPropertyValue(template, "backOffPolicy")).isInstanceOf(UniformRandomBackOffPolicy.class); } + @Test + public void testUniformRandomBackOffDuration() { + RetryTemplate template = RetryTemplate.builder() + .uniformRandomBackoff(Duration.ofSeconds(1), Duration.ofSeconds(2)) + .build(); + + UniformRandomBackOffPolicy policy = getPropertyValue(template, "backOffPolicy", + UniformRandomBackOffPolicy.class); + + assertThat(policy.getMinBackOffPeriod()).isEqualTo(1000); + assertThat(policy.getMaxBackOffPeriod()).isEqualTo(2000); + } + @Test public void testNoBackOff() { RetryTemplate template = RetryTemplate.builder().noBackoff().build(); assertThat(getPropertyValue(template, "backOffPolicy")).isInstanceOf(NoBackOffPolicy.class); } + @Test + public void testExponentialBackoff() { + RetryTemplate template = RetryTemplate.builder().exponentialBackoff(10, 2, 500).build(); + ExponentialBackOffPolicy policy = getPropertyValue(template, "backOffPolicy", ExponentialBackOffPolicy.class); + + assertThat(policy.getInitialInterval()).isEqualTo(10); + assertThat(policy.getMultiplier()).isEqualTo(2); + assertThat(policy.getMaxInterval()).isEqualTo(500); + } + + @Test + public void testExponentialBackoffDuration() { + RetryTemplate template = RetryTemplate.builder() + .exponentialBackoff(Duration.ofSeconds(2), 2, Duration.ofSeconds(3)) + .build(); + + ExponentialBackOffPolicy policy = getPropertyValue(template, "backOffPolicy", ExponentialBackOffPolicy.class); + + assertThat(policy.getInitialInterval()).isEqualTo(2000); + assertThat(policy.getMultiplier()).isEqualTo(2); + assertThat(policy.getMaxInterval()).isEqualTo(3000); + assertThat(policy.getMaxInterval()).isEqualTo(3000); + } + @Test public void testExpBackOffWithRandom() { RetryTemplate template = RetryTemplate.builder().exponentialBackoff(10, 2, 500, true).build(); assertThat(getPropertyValue(template, "backOffPolicy")).isInstanceOf(ExponentialRandomBackOffPolicy.class); } + @Test + public void testExponentialRandomBackoffDuration() { + RetryTemplate template = RetryTemplate.builder() + .exponentialBackoff(Duration.ofSeconds(2), 2, Duration.ofSeconds(3), true) + .build(); + + ExponentialRandomBackOffPolicy policy = getPropertyValue(template, "backOffPolicy", + ExponentialRandomBackOffPolicy.class); + + assertThat(policy.getInitialInterval()).isEqualTo(2000); + assertThat(policy.getMultiplier()).isEqualTo(2); + assertThat(policy.getMaxInterval()).isEqualTo(3000); + } + @Test public void testValidateInitAndMax() { assertThatIllegalArgumentException() From ae51a22cdb1eb6ac82a1da018fbe3f66d7f389d7 Mon Sep 17 00:00:00 2001 From: Andreas Ahlenstorf Date: Wed, 31 May 2023 15:18:09 +0200 Subject: [PATCH 2/2] Fix non-null assertion messages --- .../retry/support/RetryTemplateBuilder.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java b/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java index 0b2b85f0..367e9508 100644 --- a/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java +++ b/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java @@ -157,7 +157,7 @@ public RetryTemplateBuilder withTimeout(long timeoutMillis) { * @since 2.0.2 */ public RetryTemplateBuilder withTimeout(Duration timeout) { - Assert.notNull(timeout, "timeout is null"); + Assert.notNull(timeout, "timeout must not be null"); return withTimeout(timeout.toMillis()); } @@ -225,8 +225,8 @@ public RetryTemplateBuilder exponentialBackoff(long initialInterval, double mult * @since 2.0.2 */ public RetryTemplateBuilder exponentialBackoff(Duration initialInterval, double multiplier, Duration maxInterval) { - Assert.notNull(initialInterval, "initialInterval is null"); - Assert.notNull(maxInterval, "maxInterval is null"); + Assert.notNull(initialInterval, "initialInterval must not be null"); + Assert.notNull(maxInterval, "maxInterval must not be null"); return exponentialBackoff(initialInterval.toMillis(), multiplier, maxInterval.toMillis(), false); } @@ -280,8 +280,8 @@ public RetryTemplateBuilder exponentialBackoff(long initialInterval, double mult */ public RetryTemplateBuilder exponentialBackoff(Duration initialInterval, double multiplier, Duration maxInterval, boolean withRandom) { - Assert.notNull(initialInterval, "initialInterval is null"); - Assert.notNull(maxInterval, "maxInterval is null"); + Assert.notNull(initialInterval, "initialInterval most not be null"); + Assert.notNull(maxInterval, "maxInterval must not be null"); return this.exponentialBackoff(initialInterval.toMillis(), multiplier, maxInterval.toMillis(), withRandom); } @@ -310,7 +310,7 @@ public RetryTemplateBuilder fixedBackoff(long interval) { * @since 2.0.2 */ public RetryTemplateBuilder fixedBackoff(Duration interval) { - Assert.notNull(interval, "interval is null"); + Assert.notNull(interval, "interval must not be null"); long millis = interval.toMillis(); Assert.isTrue(millis >= 1, "interval is less than 1 millisecond"); @@ -349,8 +349,8 @@ public RetryTemplateBuilder uniformRandomBackoff(long minInterval, long maxInter * @since 2.0.2 */ public RetryTemplateBuilder uniformRandomBackoff(Duration minInterval, Duration maxInterval) { - Assert.notNull(minInterval, "minInterval is null"); - Assert.notNull(maxInterval, "maxInterval is null"); + Assert.notNull(minInterval, "minInterval must not be null"); + Assert.notNull(maxInterval, "maxInterval must not be null"); return this.uniformRandomBackoff(minInterval.toMillis(), maxInterval.toMillis()); }