diff --git a/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java index 5b2bd2b..bae2c8e 100644 --- a/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java +++ b/src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2018 the original author or authors. + * Copyright 2014-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,12 +46,9 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.retry.RetryListener; import org.springframework.retry.RetryPolicy; import org.springframework.retry.backoff.BackOffPolicy; -import org.springframework.retry.backoff.ExponentialBackOffPolicy; -import org.springframework.retry.backoff.ExponentialRandomBackOffPolicy; -import org.springframework.retry.backoff.FixedBackOffPolicy; +import org.springframework.retry.backoff.BackOffPolicyBuilder; import org.springframework.retry.backoff.NoBackOffPolicy; import org.springframework.retry.backoff.Sleeper; -import org.springframework.retry.backoff.UniformRandomBackOffPolicy; import org.springframework.retry.interceptor.FixedKeyGenerator; import org.springframework.retry.interceptor.MethodArgumentsKeyGenerator; import org.springframework.retry.interceptor.MethodInvocationRecoverer; @@ -401,9 +398,9 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn Double.class); } } + boolean isRandom = false; if (multiplier > 0) { - ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy(); - boolean isRandom = backoff.random(); + isRandom = backoff.random(); String randomExpression = (String) attrs.get("randomExpression"); if (StringUtils.hasText(randomExpression)) { if (ExpressionRetryPolicy.isTemplate(randomExpression)) { @@ -415,32 +412,9 @@ public class AnnotationAwareRetryOperationsInterceptor implements IntroductionIn Boolean.class); } } - if (isRandom) { - policy = new ExponentialRandomBackOffPolicy(); - } - policy.setInitialInterval(min); - policy.setMultiplier(multiplier); - policy.setMaxInterval(max > min ? max : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL); - if (this.sleeper != null) { - policy.setSleeper(this.sleeper); - } - return policy; } - if (max > min) { - UniformRandomBackOffPolicy policy = new UniformRandomBackOffPolicy(); - policy.setMinBackOffPeriod(min); - policy.setMaxBackOffPeriod(max); - if (this.sleeper != null) { - policy.setSleeper(this.sleeper); - } - return policy; - } - FixedBackOffPolicy policy = new FixedBackOffPolicy(); - policy.setBackOffPeriod(min); - if (this.sleeper != null) { - policy.setSleeper(this.sleeper); - } - return policy; + return BackOffPolicyBuilder.newBuilder().delay(min).maxDelay(max).multiplier(multiplier).random(isRandom) + .sleeper(this.sleeper).build(); } /** diff --git a/src/main/java/org/springframework/retry/backoff/BackOffPolicyBuilder.java b/src/main/java/org/springframework/retry/backoff/BackOffPolicyBuilder.java new file mode 100644 index 0000000..b9ff49f --- /dev/null +++ b/src/main/java/org/springframework/retry/backoff/BackOffPolicyBuilder.java @@ -0,0 +1,195 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.backoff; + +/** + * Fluent API for creating a {@link BackOffPolicy} based on given attributes. The delay + * values are expressed in milliseconds. If any provided value is less than one, the + * resulting policy will set it to one. The default policy is a {@link FixedBackOffPolicy} + * with a delay of 1000ms. + * + *
+ * Examples:
+ *
+ * // Default {@link FixedBackOffPolicy} with 1000ms delay
+ * BackOffPolicyBuilder
+ * .newDefaultPolicy();
+ *
+ * // {@link FixedBackOffPolicy}
+ * BackOffPolicyBuilder
+ * .newBuilder()
+ * .delay(2000)
+ * .build();
+ *
+ * // {@link UniformRandomBackOffPolicy}
+ * BackOffPolicyBuilder
+ * .newBuilder()
+ * .delay(500)
+ * .maxDelay(1000)
+ * .build();
+ *
+ * // {@link ExponentialBackOffPolicy}
+ * BackOffPolicyBuilder
+ * .newBuilder()
+ * .delay(1000)
+ * .maxDelay(5000)
+ * .multiplier(2)
+ * .build();
+ *
+ * // {@link ExponentialRandomBackOffPolicy} with provided {@link Sleeper}
+ * BackOffPolicyBuilder
+ * .newBuilder()
+ * .delay(3000)
+ * .maxDelay(5000)
+ * .multiplier(1.5)
+ * .random(true)
+ * .sleeper(mySleeper)
+ * .build();
+ *
+ * + * Not thread safe. Building should be performed in a single thread. The resulting + * {@link BackOffPolicy} however is expected to be thread-safe and designed for moderate + * load concurrent access. + * + * @author Tomaz Fernandes + * @since 1.3.3 + */ +public class BackOffPolicyBuilder { + + private static final long DEFAULT_INITIAL_DELAY = 1000L; + + private long delay = DEFAULT_INITIAL_DELAY; + + private long maxDelay; + + private double multiplier; + + private boolean random; + + private Sleeper sleeper; + + private BackOffPolicyBuilder() { + } + + /** + * Creates a new {@link BackOffPolicyBuilder} instance. + * @return the builder instance + */ + public static BackOffPolicyBuilder newBuilder() { + return new BackOffPolicyBuilder(); + } + + /** + * Creates a new {@link FixedBackOffPolicy} instance with a delay of 1000ms. + * @return the back off policy instance + */ + public static BackOffPolicy newDefaultPolicy() { + return new BackOffPolicyBuilder().build(); + } + + /** + * A canonical backoff period. Used as an initial value in the exponential case, and + * as a minimum value in the uniform case. + * @param delay the initial or canonical backoff period in milliseconds + * @return this + */ + public BackOffPolicyBuilder delay(long delay) { + this.delay = delay; + return this; + } + + /** + * The maximum wait in milliseconds between retries. If less than {@link #delay(long)} + * then a default value is applied depending on the resulting policy. + * @param maxDelay the maximum wait between retries in milliseconds + * @return this + */ + public BackOffPolicyBuilder maxDelay(long maxDelay) { + this.maxDelay = maxDelay; + return this; + } + + /** + * If positive, then used as a multiplier for generating the next delay for backoff. + * @param multiplier a multiplier to use to calculate the next backoff delay + * @return this + */ + public BackOffPolicyBuilder multiplier(double multiplier) { + this.multiplier = multiplier; + return this; + } + + /** + * In the exponential case ({@link #multiplier} > 0) set this to true to have the + * backoff delays randomized, so that the maximum delay is multiplier times the + * previous delay and the distribution is uniform between the two values. + * @param random the flag to signal randomization is required + * @return this + */ + public BackOffPolicyBuilder random(boolean random) { + this.random = random; + return this; + } + + /** + * The {@link Sleeper} instance to be used to back off. Policies default to + * {@link ThreadWaitSleeper}. + * @param sleeper the {@link Sleeper} instance + * @return this + */ + public BackOffPolicyBuilder sleeper(Sleeper sleeper) { + this.sleeper = sleeper; + return this; + } + + /** + * Builds the {@link BackOffPolicy} with the given parameters. + * @return the {@link BackOffPolicy} instance + */ + public BackOffPolicy build() { + if (this.multiplier > 0) { + ExponentialBackOffPolicy policy = new ExponentialBackOffPolicy(); + if (this.random) { + policy = new ExponentialRandomBackOffPolicy(); + } + policy.setInitialInterval(this.delay); + policy.setMultiplier(this.multiplier); + policy.setMaxInterval( + this.maxDelay > this.delay ? this.maxDelay : ExponentialBackOffPolicy.DEFAULT_MAX_INTERVAL); + if (this.sleeper != null) { + policy.setSleeper(this.sleeper); + } + return policy; + } + if (this.maxDelay > this.delay) { + UniformRandomBackOffPolicy policy = new UniformRandomBackOffPolicy(); + policy.setMinBackOffPeriod(this.delay); + policy.setMaxBackOffPeriod(this.maxDelay); + if (this.sleeper != null) { + policy.setSleeper(this.sleeper); + } + return policy; + } + FixedBackOffPolicy policy = new FixedBackOffPolicy(); + policy.setBackOffPeriod(this.delay); + if (this.sleeper != null) { + policy.setSleeper(this.sleeper); + } + return policy; + } + +} diff --git a/src/test/java/org/springframework/retry/backoff/BackOffPolicyBuilderTests.java b/src/test/java/org/springframework/retry/backoff/BackOffPolicyBuilderTests.java new file mode 100644 index 0000000..d4344d8 --- /dev/null +++ b/src/test/java/org/springframework/retry/backoff/BackOffPolicyBuilderTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.retry.backoff; + +import org.junit.Test; + +import org.springframework.beans.DirectFieldAccessor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * @author Tomaz Fernandes + * @since 1.3.3 + */ +public class BackOffPolicyBuilderTests { + + @Test + public void shouldCreateDefaultBackOffPolicy() { + BackOffPolicy backOffPolicy = BackOffPolicyBuilder.newDefaultPolicy(); + assertTrue(FixedBackOffPolicy.class.isAssignableFrom(backOffPolicy.getClass())); + FixedBackOffPolicy policy = (FixedBackOffPolicy) backOffPolicy; + assertEquals(1000, policy.getBackOffPeriod()); + } + + @Test + public void shouldCreateDefaultBackOffPolicyViaNewBuilder() { + Sleeper mockSleeper = mock(Sleeper.class); + BackOffPolicy backOffPolicy = BackOffPolicyBuilder.newBuilder().sleeper(mockSleeper).build(); + assertTrue(FixedBackOffPolicy.class.isAssignableFrom(backOffPolicy.getClass())); + FixedBackOffPolicy policy = (FixedBackOffPolicy) backOffPolicy; + assertEquals(1000, policy.getBackOffPeriod()); + assertEquals(mockSleeper, new DirectFieldAccessor(policy).getPropertyValue("sleeper")); + } + + @Test + public void shouldCreateFixedBackOffPolicy() { + Sleeper mockSleeper = mock(Sleeper.class); + BackOffPolicy backOffPolicy = BackOffPolicyBuilder.newBuilder().delay(3500).sleeper(mockSleeper).build(); + assertTrue(FixedBackOffPolicy.class.isAssignableFrom(backOffPolicy.getClass())); + FixedBackOffPolicy policy = (FixedBackOffPolicy) backOffPolicy; + assertEquals(3500, policy.getBackOffPeriod()); + assertEquals(mockSleeper, new DirectFieldAccessor(policy).getPropertyValue("sleeper")); + } + + @Test + public void shouldCreateUniformRandomBackOffPolicy() { + Sleeper mockSleeper = mock(Sleeper.class); + BackOffPolicy backOffPolicy = BackOffPolicyBuilder.newBuilder().delay(1).maxDelay(5000).sleeper(mockSleeper) + .build(); + assertTrue(UniformRandomBackOffPolicy.class.isAssignableFrom(backOffPolicy.getClass())); + UniformRandomBackOffPolicy policy = (UniformRandomBackOffPolicy) backOffPolicy; + assertEquals(1, policy.getMinBackOffPeriod()); + assertEquals(5000, policy.getMaxBackOffPeriod()); + assertEquals(mockSleeper, new DirectFieldAccessor(policy).getPropertyValue("sleeper")); + } + + @Test + public void shouldCreateExponentialBackOff() { + Sleeper mockSleeper = mock(Sleeper.class); + BackOffPolicy backOffPolicy = BackOffPolicyBuilder.newBuilder().delay(100).maxDelay(1000).multiplier(2) + .random(false).sleeper(mockSleeper).build(); + assertTrue(ExponentialBackOffPolicy.class.isAssignableFrom(backOffPolicy.getClass())); + ExponentialBackOffPolicy policy = (ExponentialBackOffPolicy) backOffPolicy; + assertEquals(100, policy.getInitialInterval()); + assertEquals(1000, policy.getMaxInterval()); + assertEquals(2, policy.getMultiplier(), 0); + assertEquals(mockSleeper, new DirectFieldAccessor(policy).getPropertyValue("sleeper")); + } + + @Test + public void shouldCreateExponentialRandomBackOff() { + Sleeper mockSleeper = mock(Sleeper.class); + BackOffPolicy backOffPolicy = BackOffPolicyBuilder.newBuilder().delay(10000).maxDelay(100000).multiplier(10) + .random(true).sleeper(mockSleeper).build(); + assertTrue(ExponentialRandomBackOffPolicy.class.isAssignableFrom(backOffPolicy.getClass())); + ExponentialRandomBackOffPolicy policy = (ExponentialRandomBackOffPolicy) backOffPolicy; + assertEquals(10000, policy.getInitialInterval()); + assertEquals(100000, policy.getMaxInterval()); + assertEquals(10, policy.getMultiplier(), 0); + assertEquals(mockSleeper, new DirectFieldAccessor(policy).getPropertyValue("sleeper")); + } + +}