GH-441: Introduce PredicateRetryPolicy
Fixes: #441 * Replace `Classifier` with more widely used `Predicate` in `RetryTemplateBuilder` * Introduce `PredicateRetryPolicy` to avoid backward incompatible changes
This commit is contained in:
committed by
Artem Bilan
parent
8339dfa472
commit
5b69d8059d
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2024-2024 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.policy;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.RetryPolicy;
|
||||
import org.springframework.retry.context.RetryContextSupport;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A policy, that is based on {@link Predicate<Throwable>}. Usually, binary classification
|
||||
* is enough for retry purposes. If you need more flexible classification, use
|
||||
* {@link ExceptionClassifierRetryPolicy}.
|
||||
*
|
||||
* @author Morulai Planinski
|
||||
* @since 2.0.7
|
||||
*/
|
||||
public class PredicateRetryPolicy implements RetryPolicy {
|
||||
|
||||
private final Predicate<Throwable> predicate;
|
||||
|
||||
public PredicateRetryPolicy(Predicate<Throwable> predicate) {
|
||||
Assert.notNull(predicate, "'predicate' must not be null");
|
||||
this.predicate = predicate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRetry(RetryContext context) {
|
||||
Throwable t = context.getLastThrowable();
|
||||
return t == null || predicate.test(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(RetryContext status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerThrowable(RetryContext context, Throwable throwable) {
|
||||
((RetryContextSupport) context).registerThrowable(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RetryContext open(RetryContext parent) {
|
||||
return new RetryContextSupport(parent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2006-2023 the original author or authors.
|
||||
* Copyright 2006-2024 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.
|
||||
@@ -13,11 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.retry.support;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.classify.BinaryExceptionClassifier;
|
||||
import org.springframework.classify.BinaryExceptionClassifierBuilder;
|
||||
@@ -33,6 +35,7 @@ import org.springframework.retry.policy.AlwaysRetryPolicy;
|
||||
import org.springframework.retry.policy.BinaryExceptionClassifierRetryPolicy;
|
||||
import org.springframework.retry.policy.CompositeRetryPolicy;
|
||||
import org.springframework.retry.policy.MaxAttemptsRetryPolicy;
|
||||
import org.springframework.retry.policy.PredicateRetryPolicy;
|
||||
import org.springframework.retry.policy.TimeoutRetryPolicy;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -75,6 +78,7 @@ import org.springframework.util.Assert;
|
||||
* @author Artem Bilan
|
||||
* @author Kim In Hoi
|
||||
* @author Andreas Ahlenstorf
|
||||
* @author Morulai Planinski
|
||||
* @since 1.3
|
||||
*/
|
||||
public class RetryTemplateBuilder {
|
||||
@@ -87,6 +91,8 @@ public class RetryTemplateBuilder {
|
||||
|
||||
private BinaryExceptionClassifierBuilder classifierBuilder;
|
||||
|
||||
private Predicate<Throwable> retryOnPredicate;
|
||||
|
||||
/* ---------------- Configure retry policy -------------- */
|
||||
|
||||
/**
|
||||
@@ -462,6 +468,27 @@ public class RetryTemplateBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link Predicate<Throwable>} that decides if the exception causes a retry.
|
||||
* <p>
|
||||
* {@code retryOn(Predicate<Throwable>)} cannot be mixed with other {@code retryOn()}
|
||||
* or {@code noRetryOn()}. Attempting to do so will result in a
|
||||
* {@link IllegalArgumentException}.
|
||||
* @param predicate if the exception causes a retry.
|
||||
* @return this
|
||||
* @throws IllegalArgumentException if {@link #retryOn} or {@link #notRetryOn} has
|
||||
* already been used.
|
||||
* @since 2.0.7
|
||||
* @see BinaryExceptionClassifierRetryPolicy
|
||||
*/
|
||||
public RetryTemplateBuilder retryOn(Predicate<Throwable> predicate) {
|
||||
Assert.isTrue(this.classifierBuilder == null && this.retryOnPredicate == null,
|
||||
"retryOn(Predicate<Throwable>) cannot be mixed with other retryOn() or noRetryOn()");
|
||||
Assert.notNull(predicate, "Predicate can not be null");
|
||||
this.retryOnPredicate = predicate;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable examining exception causes for {@link Throwable} instances that cause a
|
||||
* retry.
|
||||
@@ -537,20 +564,24 @@ public class RetryTemplateBuilder {
|
||||
public RetryTemplate build() {
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
|
||||
// Exception classifier
|
||||
|
||||
BinaryExceptionClassifier exceptionClassifier = this.classifierBuilder != null ? this.classifierBuilder.build()
|
||||
: BinaryExceptionClassifier.defaultClassifier();
|
||||
|
||||
// Retry policy
|
||||
|
||||
if (this.baseRetryPolicy == null) {
|
||||
this.baseRetryPolicy = new MaxAttemptsRetryPolicy();
|
||||
}
|
||||
|
||||
RetryPolicy exceptionRetryPolicy;
|
||||
if (this.retryOnPredicate == null) {
|
||||
BinaryExceptionClassifier exceptionClassifier = this.classifierBuilder != null
|
||||
? this.classifierBuilder.build() : BinaryExceptionClassifier.defaultClassifier();
|
||||
exceptionRetryPolicy = new BinaryExceptionClassifierRetryPolicy(exceptionClassifier);
|
||||
}
|
||||
else {
|
||||
exceptionRetryPolicy = new PredicateRetryPolicy(this.retryOnPredicate);
|
||||
}
|
||||
|
||||
CompositeRetryPolicy finalPolicy = new CompositeRetryPolicy();
|
||||
finalPolicy.setPolicies(new RetryPolicy[] { this.baseRetryPolicy,
|
||||
new BinaryExceptionClassifierRetryPolicy(exceptionClassifier) });
|
||||
finalPolicy.setPolicies(new RetryPolicy[] { this.baseRetryPolicy, exceptionRetryPolicy });
|
||||
retryTemplate.setRetryPolicy(finalPolicy);
|
||||
|
||||
// Backoff policy
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2006-2023 the original author or authors.
|
||||
* Copyright 2006-2024 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.
|
||||
@@ -22,9 +22,9 @@ import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.classify.BinaryExceptionClassifier;
|
||||
import org.springframework.retry.RetryListener;
|
||||
import org.springframework.retry.RetryPolicy;
|
||||
@@ -38,6 +38,7 @@ import org.springframework.retry.policy.BinaryExceptionClassifierRetryPolicy;
|
||||
import org.springframework.retry.policy.CompositeRetryPolicy;
|
||||
import org.springframework.retry.policy.MapRetryContextCache;
|
||||
import org.springframework.retry.policy.MaxAttemptsRetryPolicy;
|
||||
import org.springframework.retry.policy.PredicateRetryPolicy;
|
||||
import org.springframework.retry.policy.TimeoutRetryPolicy;
|
||||
import org.springframework.retry.util.test.TestUtils;
|
||||
|
||||
@@ -56,6 +57,7 @@ import static org.springframework.retry.util.test.TestUtils.getPropertyValue;
|
||||
* @author Kim In Hoi
|
||||
* @author Gary Russell
|
||||
* @author Andreas Ahlenstorf
|
||||
* @author Morulai Planinski
|
||||
*/
|
||||
public class RetryTemplateBuilderTests {
|
||||
|
||||
@@ -93,8 +95,9 @@ public class RetryTemplateBuilderTests {
|
||||
.build();
|
||||
|
||||
PolicyTuple policyTuple = PolicyTuple.extractWithAsserts(template);
|
||||
|
||||
BinaryExceptionClassifier classifier = policyTuple.exceptionClassifierRetryPolicy.getExceptionClassifier();
|
||||
assertThat(policyTuple.exceptionClassifierRetryPolicy).isInstanceOf(BinaryExceptionClassifierRetryPolicy.class);
|
||||
BinaryExceptionClassifierRetryPolicy retryPolicy = (BinaryExceptionClassifierRetryPolicy) policyTuple.exceptionClassifierRetryPolicy;
|
||||
BinaryExceptionClassifier classifier = retryPolicy.getExceptionClassifier();
|
||||
assertThat(classifier.classify(new FileNotFoundException())).isTrue();
|
||||
assertThat(classifier.classify(new IllegalArgumentException())).isTrue();
|
||||
assertThat(classifier.classify(new RuntimeException())).isFalse();
|
||||
@@ -176,7 +179,9 @@ public class RetryTemplateBuilderTests {
|
||||
}
|
||||
|
||||
private void assertDefaultClassifier(PolicyTuple policyTuple) {
|
||||
BinaryExceptionClassifier classifier = policyTuple.exceptionClassifierRetryPolicy.getExceptionClassifier();
|
||||
assertThat(policyTuple.exceptionClassifierRetryPolicy).isInstanceOf(BinaryExceptionClassifierRetryPolicy.class);
|
||||
BinaryExceptionClassifierRetryPolicy retryPolicy = (BinaryExceptionClassifierRetryPolicy) policyTuple.exceptionClassifierRetryPolicy;
|
||||
BinaryExceptionClassifier classifier = retryPolicy.getExceptionClassifier();
|
||||
assertThat(classifier.classify(new Exception())).isTrue();
|
||||
assertThat(classifier.classify(new Exception(new Error()))).isTrue();
|
||||
assertThat(classifier.classify(new Error())).isFalse();
|
||||
@@ -203,6 +208,28 @@ public class RetryTemplateBuilderTests {
|
||||
.notRetryOn(Collections.<Class<? extends Throwable>>singletonList(OutOfMemoryError.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailOnPredicateWithOtherMix() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> RetryTemplate.builder()
|
||||
.retryOn(Collections.<Class<? extends Throwable>>singletonList(IOException.class))
|
||||
.retryOn(classifiable -> true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetryOnPredicate() {
|
||||
Predicate<Throwable> predicate = classifiable -> classifiable instanceof IllegalAccessError;
|
||||
RetryTemplate template = RetryTemplate.builder().maxAttempts(10).retryOn(predicate).build();
|
||||
|
||||
PolicyTuple policyTuple = PolicyTuple.extractWithAsserts(template);
|
||||
assertThat(policyTuple.exceptionClassifierRetryPolicy).isInstanceOf(PredicateRetryPolicy.class);
|
||||
RetryPolicy retryPolicy = policyTuple.exceptionClassifierRetryPolicy;
|
||||
assertThat(retryPolicy).isInstanceOf(PredicateRetryPolicy.class);
|
||||
assertThat(policyTuple.baseRetryPolicy).isInstanceOf(MaxAttemptsRetryPolicy.class);
|
||||
assertThat(policyTuple.baseRetryPolicy.getMaxAttempts()).isEqualTo(10);
|
||||
assertThat(getPropertyValue(template, "listeners", RetryListener[].class)).isEmpty();
|
||||
assertThat(getPropertyValue(template, "backOffPolicy")).isInstanceOf(NoBackOffPolicy.class);
|
||||
}
|
||||
|
||||
/* ---------------- BackOff -------------- */
|
||||
|
||||
@Test
|
||||
@@ -325,7 +352,7 @@ public class RetryTemplateBuilderTests {
|
||||
|
||||
RetryPolicy baseRetryPolicy;
|
||||
|
||||
BinaryExceptionClassifierRetryPolicy exceptionClassifierRetryPolicy;
|
||||
RetryPolicy exceptionClassifierRetryPolicy;
|
||||
|
||||
static PolicyTuple extractWithAsserts(RetryTemplate template) {
|
||||
CompositeRetryPolicy compositeRetryPolicy = getPropertyValue(template, "retryPolicy",
|
||||
@@ -335,8 +362,8 @@ public class RetryTemplateBuilderTests {
|
||||
assertThat(getPropertyValue(compositeRetryPolicy, "optimistic", Boolean.class)).isFalse();
|
||||
|
||||
for (final RetryPolicy policy : getPropertyValue(compositeRetryPolicy, "policies", RetryPolicy[].class)) {
|
||||
if (policy instanceof BinaryExceptionClassifierRetryPolicy) {
|
||||
res.exceptionClassifierRetryPolicy = (BinaryExceptionClassifierRetryPolicy) policy;
|
||||
if (policy instanceof BinaryExceptionClassifierRetryPolicy || policy instanceof PredicateRetryPolicy) {
|
||||
res.exceptionClassifierRetryPolicy = policy;
|
||||
}
|
||||
else {
|
||||
res.baseRetryPolicy = policy;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2006-2023 the original author or authors.
|
||||
* Copyright 2006-2024 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.
|
||||
@@ -52,6 +52,7 @@ import static org.mockito.Mockito.verify;
|
||||
* @author Gary Russell
|
||||
* @author Henning Pöttker
|
||||
* @author Emanuele Ivaldi
|
||||
* @author Morulai Planinski
|
||||
*/
|
||||
public class RetryTemplateTests {
|
||||
|
||||
@@ -92,6 +93,38 @@ public class RetryTemplateTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetryOnPredicateWithRetry() throws Throwable {
|
||||
for (int x = 1; x <= 10; x++) {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
callback.setAttemptsBeforeSuccess(x);
|
||||
callback.setExceptionToThrow(new IllegalStateException("retry"));
|
||||
RetryTemplate retryTemplate = RetryTemplate.builder()
|
||||
.maxAttempts(x)
|
||||
.retryOn(classifiable -> classifiable instanceof IllegalStateException
|
||||
&& classifiable.getMessage().equals("retry"))
|
||||
.build();
|
||||
|
||||
retryTemplate.execute(callback);
|
||||
assertThat(callback.attempts).isEqualTo(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetryOnPredicateWithoutRetry() throws Throwable {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
callback.setAttemptsBeforeSuccess(0);
|
||||
callback.setExceptionToThrow(new IllegalStateException("no retry"));
|
||||
RetryTemplate retryTemplate = RetryTemplate.builder()
|
||||
.maxAttempts(3)
|
||||
.retryOn(classifiable -> classifiable instanceof IllegalStateException
|
||||
&& classifiable.getMessage().equals("retry"))
|
||||
.build();
|
||||
|
||||
retryTemplate.execute(callback);
|
||||
assertThat(callback.attempts).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessfulRecovery() throws Throwable {
|
||||
MockRetryCallback callback = new MockRetryCallback();
|
||||
|
||||
Reference in New Issue
Block a user