Skip to content

Commit

Permalink
GH-441: Introduce PredicateRetryPolicy
Browse files Browse the repository at this point in the history
Fixes: #441

* Replace `Classifier` with more widely used `Predicate` in `RetryTemplateBuilder`
* Introduce `PredicateRetryPolicy` to avoid backward incompatible changes
  • Loading branch information
morulay authored and artembilan committed Jun 4, 2024
1 parent 8339dfa commit 5b69d80
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -33,6 +35,7 @@
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;

Expand Down Expand Up @@ -75,6 +78,7 @@
* @author Artem Bilan
* @author Kim In Hoi
* @author Andreas Ahlenstorf
* @author Morulai Planinski
* @since 1.3
*/
public class RetryTemplateBuilder {
Expand All @@ -87,6 +91,8 @@ public class RetryTemplateBuilder {

private BinaryExceptionClassifierBuilder classifierBuilder;

private Predicate<Throwable> retryOnPredicate;

/* ---------------- Configure retry policy -------------- */

/**
Expand Down Expand Up @@ -462,6 +468,27 @@ public RetryTemplateBuilder notRetryOn(List<Class<? extends Throwable>> throwabl
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.
Expand Down Expand Up @@ -537,20 +564,24 @@ public RetryTemplateBuilder withListeners(List<RetryListener> listeners) {
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -22,9 +22,9 @@
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;
Expand All @@ -38,6 +38,7 @@
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;

Expand All @@ -56,6 +57,7 @@
* @author Kim In Hoi
* @author Gary Russell
* @author Andreas Ahlenstorf
* @author Morulai Planinski
*/
public class RetryTemplateBuilderTests {

Expand Down Expand Up @@ -93,8 +95,9 @@ public void testBasicCustomization() {
.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();
Expand Down Expand Up @@ -176,7 +179,9 @@ public void testCustomPolicy() {
}

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();
Expand All @@ -203,6 +208,28 @@ public void testFailOnNotationsMix() {
.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
Expand Down Expand Up @@ -325,7 +352,7 @@ private static class PolicyTuple {

RetryPolicy baseRetryPolicy;

BinaryExceptionClassifierRetryPolicy exceptionClassifierRetryPolicy;
RetryPolicy exceptionClassifierRetryPolicy;

static PolicyTuple extractWithAsserts(RetryTemplate template) {
CompositeRetryPolicy compositeRetryPolicy = getPropertyValue(template, "retryPolicy",
Expand All @@ -335,8 +362,8 @@ static PolicyTuple extractWithAsserts(RetryTemplate template) {
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -52,6 +52,7 @@
* @author Gary Russell
* @author Henning Pöttker
* @author Emanuele Ivaldi
* @author Morulai Planinski
*/
public class RetryTemplateTests {

Expand Down Expand Up @@ -92,6 +93,38 @@ public void testSpecificExceptionRetry() {
}
}

@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();
Expand Down

0 comments on commit 5b69d80

Please sign in to comment.