diff --git a/src/main/java/org/springframework/retry/policy/PredicateRetryPolicy.java b/src/main/java/org/springframework/retry/policy/PredicateRetryPolicy.java new file mode 100644 index 0000000..b1bc7b4 --- /dev/null +++ b/src/main/java/org/springframework/retry/policy/PredicateRetryPolicy.java @@ -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}. 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 predicate; + + public PredicateRetryPolicy(Predicate 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); + } + +} diff --git a/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java b/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java index 9d198c3..17f4423 100644 --- a/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java +++ b/src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java @@ -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 retryOnPredicate; + /* ---------------- Configure retry policy -------------- */ /** @@ -462,6 +468,27 @@ public class RetryTemplateBuilder { return this; } + /** + * Set a {@link Predicate} that decides if the exception causes a retry. + *

+ * {@code retryOn(Predicate)} 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 predicate) { + Assert.isTrue(this.classifierBuilder == null && this.retryOnPredicate == null, + "retryOn(Predicate) 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 diff --git a/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java b/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java index f098417..eb29242 100644 --- a/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java +++ b/src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java @@ -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.>singletonList(OutOfMemoryError.class))); } + @Test + public void testFailOnPredicateWithOtherMix() { + assertThatIllegalArgumentException().isThrownBy(() -> RetryTemplate.builder() + .retryOn(Collections.>singletonList(IOException.class)) + .retryOn(classifiable -> true)); + } + + @Test + public void testRetryOnPredicate() { + Predicate 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; diff --git a/src/test/java/org/springframework/retry/support/RetryTemplateTests.java b/src/test/java/org/springframework/retry/support/RetryTemplateTests.java index 1828cb3..5030b12 100644 --- a/src/test/java/org/springframework/retry/support/RetryTemplateTests.java +++ b/src/test/java/org/springframework/retry/support/RetryTemplateTests.java @@ -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();