GH-276: Introduce BackOffPolicyBuilder

Fixes https://github.com/spring-projects/spring-retry/issues/276

* Add `BackOffPolicyBuilder` implementation to expose `BackOffPolicy` building logic to other projects
* Address review suggestions
* Add method for default policy
* Improve API
This commit is contained in:
Tomaz Fernandes
2022-03-11 15:20:35 -03:00
committed by GitHub
parent 14c7217bbc
commit b751d5d13d
3 changed files with 300 additions and 32 deletions

View File

@@ -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();
}
/**

View File

@@ -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.
*
* <p>
* Examples: <pre>
*
* // 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();
* </pre>
* <p>
* 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} &gt; 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;
}
}

View File

@@ -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"));
}
}