Skip to content

Commit 5b69d80

Browse files
morulayartembilan
authored andcommitted
GH-441: Introduce PredicateRetryPolicy
Fixes: #441 * Replace `Classifier` with more widely used `Predicate` in `RetryTemplateBuilder` * Introduce `PredicateRetryPolicy` to avoid backward incompatible changes
1 parent 8339dfa commit 5b69d80

File tree

4 files changed

+171
-17
lines changed

4 files changed

+171
-17
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2024-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.retry.policy;
18+
19+
import java.util.function.Predicate;
20+
21+
import org.springframework.retry.RetryContext;
22+
import org.springframework.retry.RetryPolicy;
23+
import org.springframework.retry.context.RetryContextSupport;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* A policy, that is based on {@link Predicate<Throwable>}. Usually, binary classification
28+
* is enough for retry purposes. If you need more flexible classification, use
29+
* {@link ExceptionClassifierRetryPolicy}.
30+
*
31+
* @author Morulai Planinski
32+
* @since 2.0.7
33+
*/
34+
public class PredicateRetryPolicy implements RetryPolicy {
35+
36+
private final Predicate<Throwable> predicate;
37+
38+
public PredicateRetryPolicy(Predicate<Throwable> predicate) {
39+
Assert.notNull(predicate, "'predicate' must not be null");
40+
this.predicate = predicate;
41+
}
42+
43+
@Override
44+
public boolean canRetry(RetryContext context) {
45+
Throwable t = context.getLastThrowable();
46+
return t == null || predicate.test(t);
47+
}
48+
49+
@Override
50+
public void close(RetryContext status) {
51+
}
52+
53+
@Override
54+
public void registerThrowable(RetryContext context, Throwable throwable) {
55+
((RetryContextSupport) context).registerThrowable(throwable);
56+
}
57+
58+
@Override
59+
public RetryContext open(RetryContext parent) {
60+
return new RetryContextSupport(parent);
61+
}
62+
63+
}

src/main/java/org/springframework/retry/support/RetryTemplateBuilder.java

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -13,11 +13,13 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
1617
package org.springframework.retry.support;
1718

1819
import java.time.Duration;
1920
import java.util.ArrayList;
2021
import java.util.List;
22+
import java.util.function.Predicate;
2123

2224
import org.springframework.classify.BinaryExceptionClassifier;
2325
import org.springframework.classify.BinaryExceptionClassifierBuilder;
@@ -33,6 +35,7 @@
3335
import org.springframework.retry.policy.BinaryExceptionClassifierRetryPolicy;
3436
import org.springframework.retry.policy.CompositeRetryPolicy;
3537
import org.springframework.retry.policy.MaxAttemptsRetryPolicy;
38+
import org.springframework.retry.policy.PredicateRetryPolicy;
3639
import org.springframework.retry.policy.TimeoutRetryPolicy;
3740
import org.springframework.util.Assert;
3841

@@ -75,6 +78,7 @@
7578
* @author Artem Bilan
7679
* @author Kim In Hoi
7780
* @author Andreas Ahlenstorf
81+
* @author Morulai Planinski
7882
* @since 1.3
7983
*/
8084
public class RetryTemplateBuilder {
@@ -87,6 +91,8 @@ public class RetryTemplateBuilder {
8791

8892
private BinaryExceptionClassifierBuilder classifierBuilder;
8993

94+
private Predicate<Throwable> retryOnPredicate;
95+
9096
/* ---------------- Configure retry policy -------------- */
9197

9298
/**
@@ -462,6 +468,27 @@ public RetryTemplateBuilder notRetryOn(List<Class<? extends Throwable>> throwabl
462468
return this;
463469
}
464470

471+
/**
472+
* Set a {@link Predicate<Throwable>} that decides if the exception causes a retry.
473+
* <p>
474+
* {@code retryOn(Predicate<Throwable>)} cannot be mixed with other {@code retryOn()}
475+
* or {@code noRetryOn()}. Attempting to do so will result in a
476+
* {@link IllegalArgumentException}.
477+
* @param predicate if the exception causes a retry.
478+
* @return this
479+
* @throws IllegalArgumentException if {@link #retryOn} or {@link #notRetryOn} has
480+
* already been used.
481+
* @since 2.0.7
482+
* @see BinaryExceptionClassifierRetryPolicy
483+
*/
484+
public RetryTemplateBuilder retryOn(Predicate<Throwable> predicate) {
485+
Assert.isTrue(this.classifierBuilder == null && this.retryOnPredicate == null,
486+
"retryOn(Predicate<Throwable>) cannot be mixed with other retryOn() or noRetryOn()");
487+
Assert.notNull(predicate, "Predicate can not be null");
488+
this.retryOnPredicate = predicate;
489+
return this;
490+
}
491+
465492
/**
466493
* Enable examining exception causes for {@link Throwable} instances that cause a
467494
* retry.
@@ -537,20 +564,24 @@ public RetryTemplateBuilder withListeners(List<RetryListener> listeners) {
537564
public RetryTemplate build() {
538565
RetryTemplate retryTemplate = new RetryTemplate();
539566

540-
// Exception classifier
541-
542-
BinaryExceptionClassifier exceptionClassifier = this.classifierBuilder != null ? this.classifierBuilder.build()
543-
: BinaryExceptionClassifier.defaultClassifier();
544-
545567
// Retry policy
546568

547569
if (this.baseRetryPolicy == null) {
548570
this.baseRetryPolicy = new MaxAttemptsRetryPolicy();
549571
}
550572

573+
RetryPolicy exceptionRetryPolicy;
574+
if (this.retryOnPredicate == null) {
575+
BinaryExceptionClassifier exceptionClassifier = this.classifierBuilder != null
576+
? this.classifierBuilder.build() : BinaryExceptionClassifier.defaultClassifier();
577+
exceptionRetryPolicy = new BinaryExceptionClassifierRetryPolicy(exceptionClassifier);
578+
}
579+
else {
580+
exceptionRetryPolicy = new PredicateRetryPolicy(this.retryOnPredicate);
581+
}
582+
551583
CompositeRetryPolicy finalPolicy = new CompositeRetryPolicy();
552-
finalPolicy.setPolicies(new RetryPolicy[] { this.baseRetryPolicy,
553-
new BinaryExceptionClassifierRetryPolicy(exceptionClassifier) });
584+
finalPolicy.setPolicies(new RetryPolicy[] { this.baseRetryPolicy, exceptionRetryPolicy });
554585
retryTemplate.setRetryPolicy(finalPolicy);
555586

556587
// Backoff policy

src/test/java/org/springframework/retry/support/RetryTemplateBuilderTests.java

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,9 +22,9 @@
2222
import java.util.Arrays;
2323
import java.util.Collections;
2424
import java.util.List;
25+
import java.util.function.Predicate;
2526

2627
import org.junit.jupiter.api.Test;
27-
2828
import org.springframework.classify.BinaryExceptionClassifier;
2929
import org.springframework.retry.RetryListener;
3030
import org.springframework.retry.RetryPolicy;
@@ -38,6 +38,7 @@
3838
import org.springframework.retry.policy.CompositeRetryPolicy;
3939
import org.springframework.retry.policy.MapRetryContextCache;
4040
import org.springframework.retry.policy.MaxAttemptsRetryPolicy;
41+
import org.springframework.retry.policy.PredicateRetryPolicy;
4142
import org.springframework.retry.policy.TimeoutRetryPolicy;
4243
import org.springframework.retry.util.test.TestUtils;
4344

@@ -56,6 +57,7 @@
5657
* @author Kim In Hoi
5758
* @author Gary Russell
5859
* @author Andreas Ahlenstorf
60+
* @author Morulai Planinski
5961
*/
6062
public class RetryTemplateBuilderTests {
6163

@@ -93,8 +95,9 @@ public void testBasicCustomization() {
9395
.build();
9496

9597
PolicyTuple policyTuple = PolicyTuple.extractWithAsserts(template);
96-
97-
BinaryExceptionClassifier classifier = policyTuple.exceptionClassifierRetryPolicy.getExceptionClassifier();
98+
assertThat(policyTuple.exceptionClassifierRetryPolicy).isInstanceOf(BinaryExceptionClassifierRetryPolicy.class);
99+
BinaryExceptionClassifierRetryPolicy retryPolicy = (BinaryExceptionClassifierRetryPolicy) policyTuple.exceptionClassifierRetryPolicy;
100+
BinaryExceptionClassifier classifier = retryPolicy.getExceptionClassifier();
98101
assertThat(classifier.classify(new FileNotFoundException())).isTrue();
99102
assertThat(classifier.classify(new IllegalArgumentException())).isTrue();
100103
assertThat(classifier.classify(new RuntimeException())).isFalse();
@@ -176,7 +179,9 @@ public void testCustomPolicy() {
176179
}
177180

178181
private void assertDefaultClassifier(PolicyTuple policyTuple) {
179-
BinaryExceptionClassifier classifier = policyTuple.exceptionClassifierRetryPolicy.getExceptionClassifier();
182+
assertThat(policyTuple.exceptionClassifierRetryPolicy).isInstanceOf(BinaryExceptionClassifierRetryPolicy.class);
183+
BinaryExceptionClassifierRetryPolicy retryPolicy = (BinaryExceptionClassifierRetryPolicy) policyTuple.exceptionClassifierRetryPolicy;
184+
BinaryExceptionClassifier classifier = retryPolicy.getExceptionClassifier();
180185
assertThat(classifier.classify(new Exception())).isTrue();
181186
assertThat(classifier.classify(new Exception(new Error()))).isTrue();
182187
assertThat(classifier.classify(new Error())).isFalse();
@@ -203,6 +208,28 @@ public void testFailOnNotationsMix() {
203208
.notRetryOn(Collections.<Class<? extends Throwable>>singletonList(OutOfMemoryError.class)));
204209
}
205210

211+
@Test
212+
public void testFailOnPredicateWithOtherMix() {
213+
assertThatIllegalArgumentException().isThrownBy(() -> RetryTemplate.builder()
214+
.retryOn(Collections.<Class<? extends Throwable>>singletonList(IOException.class))
215+
.retryOn(classifiable -> true));
216+
}
217+
218+
@Test
219+
public void testRetryOnPredicate() {
220+
Predicate<Throwable> predicate = classifiable -> classifiable instanceof IllegalAccessError;
221+
RetryTemplate template = RetryTemplate.builder().maxAttempts(10).retryOn(predicate).build();
222+
223+
PolicyTuple policyTuple = PolicyTuple.extractWithAsserts(template);
224+
assertThat(policyTuple.exceptionClassifierRetryPolicy).isInstanceOf(PredicateRetryPolicy.class);
225+
RetryPolicy retryPolicy = policyTuple.exceptionClassifierRetryPolicy;
226+
assertThat(retryPolicy).isInstanceOf(PredicateRetryPolicy.class);
227+
assertThat(policyTuple.baseRetryPolicy).isInstanceOf(MaxAttemptsRetryPolicy.class);
228+
assertThat(policyTuple.baseRetryPolicy.getMaxAttempts()).isEqualTo(10);
229+
assertThat(getPropertyValue(template, "listeners", RetryListener[].class)).isEmpty();
230+
assertThat(getPropertyValue(template, "backOffPolicy")).isInstanceOf(NoBackOffPolicy.class);
231+
}
232+
206233
/* ---------------- BackOff -------------- */
207234

208235
@Test
@@ -325,7 +352,7 @@ private static class PolicyTuple {
325352

326353
RetryPolicy baseRetryPolicy;
327354

328-
BinaryExceptionClassifierRetryPolicy exceptionClassifierRetryPolicy;
355+
RetryPolicy exceptionClassifierRetryPolicy;
329356

330357
static PolicyTuple extractWithAsserts(RetryTemplate template) {
331358
CompositeRetryPolicy compositeRetryPolicy = getPropertyValue(template, "retryPolicy",
@@ -335,8 +362,8 @@ static PolicyTuple extractWithAsserts(RetryTemplate template) {
335362
assertThat(getPropertyValue(compositeRetryPolicy, "optimistic", Boolean.class)).isFalse();
336363

337364
for (final RetryPolicy policy : getPropertyValue(compositeRetryPolicy, "policies", RetryPolicy[].class)) {
338-
if (policy instanceof BinaryExceptionClassifierRetryPolicy) {
339-
res.exceptionClassifierRetryPolicy = (BinaryExceptionClassifierRetryPolicy) policy;
365+
if (policy instanceof BinaryExceptionClassifierRetryPolicy || policy instanceof PredicateRetryPolicy) {
366+
res.exceptionClassifierRetryPolicy = policy;
340367
}
341368
else {
342369
res.baseRetryPolicy = policy;

src/test/java/org/springframework/retry/support/RetryTemplateTests.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -52,6 +52,7 @@
5252
* @author Gary Russell
5353
* @author Henning Pöttker
5454
* @author Emanuele Ivaldi
55+
* @author Morulai Planinski
5556
*/
5657
public class RetryTemplateTests {
5758

@@ -92,6 +93,38 @@ public void testSpecificExceptionRetry() {
9293
}
9394
}
9495

96+
@Test
97+
public void testRetryOnPredicateWithRetry() throws Throwable {
98+
for (int x = 1; x <= 10; x++) {
99+
MockRetryCallback callback = new MockRetryCallback();
100+
callback.setAttemptsBeforeSuccess(x);
101+
callback.setExceptionToThrow(new IllegalStateException("retry"));
102+
RetryTemplate retryTemplate = RetryTemplate.builder()
103+
.maxAttempts(x)
104+
.retryOn(classifiable -> classifiable instanceof IllegalStateException
105+
&& classifiable.getMessage().equals("retry"))
106+
.build();
107+
108+
retryTemplate.execute(callback);
109+
assertThat(callback.attempts).isEqualTo(x);
110+
}
111+
}
112+
113+
@Test
114+
public void testRetryOnPredicateWithoutRetry() throws Throwable {
115+
MockRetryCallback callback = new MockRetryCallback();
116+
callback.setAttemptsBeforeSuccess(0);
117+
callback.setExceptionToThrow(new IllegalStateException("no retry"));
118+
RetryTemplate retryTemplate = RetryTemplate.builder()
119+
.maxAttempts(3)
120+
.retryOn(classifiable -> classifiable instanceof IllegalStateException
121+
&& classifiable.getMessage().equals("retry"))
122+
.build();
123+
124+
retryTemplate.execute(callback);
125+
assertThat(callback.attempts).isEqualTo(1);
126+
}
127+
95128
@Test
96129
public void testSuccessfulRecovery() throws Throwable {
97130
MockRetryCallback callback = new MockRetryCallback();

0 commit comments

Comments
 (0)