Skip to content

Commit 860bd0d

Browse files
garyrussellartembilan
authored andcommitted
GH-264: Enable Skipping Recovery
Resolves #264
1 parent fef3cfe commit 860bd0d

File tree

10 files changed

+186
-35
lines changed

10 files changed

+186
-35
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,24 @@ When evaluating at runtime, a root object containing the method arguments is pas
597597
**Note:** The arguments are not available until the method has been called at least once; they will be null initially, which means, for example, you can't set the initial `maxAttempts` using an argument value, you can, however, change the `maxAttempts` after the first failure and before any retries are performed.
598598
Also, the arguments are only available when using stateless retry (which includes the `@CircuitBreaker`).
599599

600+
Version 2.0 adds more flexibility to exception classification.
601+
602+
```java
603+
@Retryable(retryFor = RuntimeException.class, noRetryFor = IllegalStateException.class, notRecoverable = {
604+
IllegalArgumentException.class, IllegalStateException.class })
605+
public void service() {
606+
...
607+
}
608+
609+
@Recover
610+
public void recover(Throwable cause) {
611+
...
612+
}
613+
```
614+
615+
`retryFor` and `noRetryFor` are replacements of `include` and `exclude` properties, which are now deprecated.
616+
The new `notRecoverable` property allows the recovery method(s) to be skipped, even if one matches the exception type; the exception is thrown to the caller either after retries are exhausted, or immediately, if the exception is not retryable.
617+
600618
##### Examples
601619

602620
```java

src/main/java/org/springframework/retry/RetryContext.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2007 the original author or authors.
2+
* Copyright 2006-2022 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.
@@ -55,6 +55,12 @@ public interface RetryContext extends AttributeAccessor {
5555
*/
5656
String EXHAUSTED = "context.exhausted";
5757

58+
/**
59+
* Retry context attribute that is non-null (and true) if the exception is not
60+
* recoverable.
61+
*/
62+
String NO_RECOVERY = "context.no-recovery";
63+
5864
/**
5965
* Signal to the framework that no more attempts should be made to try or retry the
6066
* current {@link RetryCallback}.

src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -344,11 +344,11 @@ private RetryPolicy getRetryPolicy(Annotation retryable, boolean stateless) {
344344
boolean hasExpression = StringUtils.hasText(exceptionExpression);
345345
if (includes.length == 0) {
346346
@SuppressWarnings("unchecked")
347-
Class<? extends Throwable>[] value = (Class<? extends Throwable>[]) attrs.get("include");
347+
Class<? extends Throwable>[] value = (Class<? extends Throwable>[]) attrs.get("retryFor");
348348
includes = value;
349349
}
350350
@SuppressWarnings("unchecked")
351-
Class<? extends Throwable>[] excludes = (Class<? extends Throwable>[]) attrs.get("exclude");
351+
Class<? extends Throwable>[] excludes = (Class<? extends Throwable>[]) attrs.get("noRetryFor");
352352
Integer maxAttempts = (Integer) attrs.get("maxAttempts");
353353
String maxAttemptsExpression = (String) attrs.get("maxAttemptsExpression");
354354
Expression parsedExpression = null;
@@ -360,8 +360,9 @@ private RetryPolicy getRetryPolicy(Annotation retryable, boolean stateless) {
360360
}
361361
}
362362
final Expression expression = parsedExpression;
363+
SimpleRetryPolicy simple = null;
363364
if (includes.length == 0 && excludes.length == 0) {
364-
SimpleRetryPolicy simple = hasExpression
365+
simple = hasExpression
365366
? new ExpressionRetryPolicy(resolve(exceptionExpression)).withBeanFactory(this.beanFactory)
366367
: new SimpleRetryPolicy();
367368
if (expression != null) {
@@ -370,7 +371,6 @@ private RetryPolicy getRetryPolicy(Annotation retryable, boolean stateless) {
370371
else {
371372
simple.setMaxAttempts(maxAttempts);
372373
}
373-
return simple;
374374
}
375375
Map<Class<? extends Throwable>, Boolean> policyMap = new HashMap<>();
376376
for (Class<? extends Throwable> type : includes) {
@@ -380,17 +380,24 @@ private RetryPolicy getRetryPolicy(Annotation retryable, boolean stateless) {
380380
policyMap.put(type, false);
381381
}
382382
boolean retryNotExcluded = includes.length == 0;
383-
if (hasExpression) {
384-
return new ExpressionRetryPolicy(maxAttempts, policyMap, true, exceptionExpression, retryNotExcluded)
385-
.withBeanFactory(this.beanFactory);
386-
}
387-
else {
388-
SimpleRetryPolicy policy = new SimpleRetryPolicy(maxAttempts, policyMap, true, retryNotExcluded);
389-
if (expression != null) {
390-
policy.setMaxAttempts(() -> evaluate(expression, Integer.class, stateless));
383+
if (simple == null) {
384+
if (hasExpression) {
385+
simple = new ExpressionRetryPolicy(maxAttempts, policyMap, true, resolve(exceptionExpression),
386+
retryNotExcluded).withBeanFactory(this.beanFactory);
387+
}
388+
else {
389+
simple = new SimpleRetryPolicy(maxAttempts, policyMap, true, retryNotExcluded);
390+
if (expression != null) {
391+
simple.setMaxAttempts(() -> evaluate(expression, Integer.class, stateless));
392+
}
391393
}
392-
return policy;
393394
}
395+
@SuppressWarnings("unchecked")
396+
Class<? extends Throwable>[] noRecovery = (Class<? extends Throwable>[]) attrs.get("notRecoverable");
397+
if (noRecovery != null && noRecovery.length > 0) {
398+
simple.setNotRecoverable(noRecovery);
399+
}
400+
return simple;
394401
}
395402

396403
private BackOffPolicy getBackoffPolicy(Backoff backoff, boolean stateless) {

src/main/java/org/springframework/retry/annotation/CircuitBreaker.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424

25+
import org.springframework.core.annotation.AliasFor;
26+
2527
/**
2628
* Annotation for a method invocation that is retryable.
2729
*
@@ -48,17 +50,51 @@
4850
* Exception types that are retryable. Defaults to empty (and if excludes is also
4951
* empty all exceptions are retried).
5052
* @return exception types to retry
53+
* @deprecated in favor of {@link #retryFor()}.
5154
*/
55+
@AliasFor("retryFor")
56+
@Deprecated
5257
Class<? extends Throwable>[] include() default {};
5358

59+
/**
60+
* Exception types that are retryable. Defaults to empty (and, if noRetryFor is also
61+
* empty, all exceptions are retried).
62+
* @return exception types to retry
63+
* @since 2.0
64+
*/
65+
@AliasFor("include")
66+
Class<? extends Throwable>[] retryFor() default {};
67+
5468
/**
5569
* Exception types that are not retryable. Defaults to empty (and if includes is also
5670
* empty all exceptions are retried). If includes is empty but excludes is not, all
5771
* not excluded exceptions are retried
5872
* @return exception types not to retry
73+
* @deprecated in favor of {@link #noRetryFor()}.
5974
*/
75+
@Deprecated
76+
@AliasFor("noRetryFor")
6077
Class<? extends Throwable>[] exclude() default {};
6178

79+
/**
80+
* Exception types that are not retryable. Defaults to empty (and, if retryFor is also
81+
* empty, all exceptions are retried). If retryFor is empty but excludes is not, all
82+
* other exceptions are retried
83+
* @return exception types not to retry
84+
* @since 2.0
85+
*/
86+
@AliasFor("exclude")
87+
Class<? extends Throwable>[] noRetryFor() default {};
88+
89+
/**
90+
* Exception types that are not recoverable; these exceptions are thrown to the caller
91+
* without calling any recoverer (immediately if also in {@link #noRetryFor()}).
92+
* Defaults to empty.
93+
* @return exception types not to retry
94+
* @since 2.0
95+
*/
96+
Class<? extends Throwable>[] notRecoverable() default {};
97+
6298
/**
6399
* @return the maximum number of attempts (including the first failure), defaults to 3
64100
*/

src/main/java/org/springframework/retry/annotation/Retryable.java

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424

25+
import org.springframework.core.annotation.AliasFor;
26+
2527
/**
2628
* Annotation for a method invocation that is retryable.
2729
*
@@ -52,27 +54,63 @@
5254
String interceptor() default "";
5355

5456
/**
55-
* Exception types that are retryable. Synonym for includes(). Defaults to empty (and
57+
* Exception types that are retryable. Synonym for include(). Defaults to empty (and
5658
* if excludes is also empty all exceptions are retried).
5759
* @return exception types to retry
60+
* @deprecated in favor of {@link #retryFor()}
5861
*/
62+
@Deprecated
5963
Class<? extends Throwable>[] value() default {};
6064

6165
/**
62-
* Exception types that are retryable. Defaults to empty (and if excludes is also
63-
* empty all exceptions are retried).
66+
* Exception types that are retryable. Defaults to empty (and, if exclude is also
67+
* empty, all exceptions are retried).
6468
* @return exception types to retry
69+
* @deprecated in favor of {@link #retryFor()}.
6570
*/
71+
@AliasFor("retryFor")
72+
@Deprecated
6673
Class<? extends Throwable>[] include() default {};
6774

6875
/**
69-
* Exception types that are not retryable. Defaults to empty (and if includes is also
76+
* Exception types that are retryable. Defaults to empty (and, if noRetryFor is also
77+
* empty, all exceptions are retried).
78+
* @return exception types to retry
79+
* @since 2.0
80+
*/
81+
@AliasFor("include")
82+
Class<? extends Throwable>[] retryFor() default {};
83+
84+
/**
85+
* Exception types that are not retryable. Defaults to empty (and if include is also
7086
* empty all exceptions are retried). If includes is empty but excludes is not, all
7187
* not excluded exceptions are retried
7288
* @return exception types not to retry
89+
* @deprecated in favor of {@link #noRetryFor()}.
7390
*/
91+
@Deprecated
92+
@AliasFor("noRetryFor")
7493
Class<? extends Throwable>[] exclude() default {};
7594

95+
/**
96+
* Exception types that are not retryable. Defaults to empty (and, if retryFor is also
97+
* empty, all exceptions are retried). If retryFor is empty but excludes is not, all
98+
* other exceptions are retried
99+
* @return exception types not to retry
100+
* @since 2.0
101+
*/
102+
@AliasFor("exclude")
103+
Class<? extends Throwable>[] noRetryFor() default {};
104+
105+
/**
106+
* Exception types that are not recoverable; these exceptions are thrown to the caller
107+
* without calling any recoverer (immediately if also in {@link #noRetryFor()}).
108+
* Defaults to empty.
109+
* @return exception types not to retry
110+
* @since 2.0
111+
*/
112+
Class<? extends Throwable>[] notRecoverable() default {};
113+
76114
/**
77115
* A unique label for statistics reporting. If not provided the caller may choose to
78116
* ignore it, or provide a default.

src/main/java/org/springframework/retry/policy/SimpleRetryPolicy.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.retry.policy;
1818

19+
import java.util.Collections;
20+
import java.util.HashMap;
1921
import java.util.Map;
2022
import java.util.function.Supplier;
2123

@@ -70,6 +72,9 @@ public class SimpleRetryPolicy implements RetryPolicy {
7072

7173
private BinaryExceptionClassifier retryableClassifier = new BinaryExceptionClassifier(false);
7274

75+
private BinaryExceptionClassifier recoverableClassifier = new BinaryExceptionClassifier(Collections.emptyMap(),
76+
true, true);
77+
7378
/**
7479
* Create a {@link SimpleRetryPolicy} with the default number of retry attempts,
7580
* retrying all exceptions.
@@ -153,6 +158,20 @@ public void setMaxAttempts(int maxAttempts) {
153158
this.maxAttempts = maxAttempts;
154159
}
155160

161+
/**
162+
* Configure throwables that should not be passed to a recoverer (if present) but
163+
* thrown immediately.
164+
* @param noRecovery the throwables.
165+
* @since 3.0
166+
*/
167+
public void setNotRecoverable(Class<? extends Throwable>... noRecovery) {
168+
Map<Class<? extends Throwable>, Boolean> map = new HashMap<>();
169+
for (Class<? extends Throwable> clazz : noRecovery) {
170+
map.put(clazz, false);
171+
}
172+
this.recoverableClassifier = new BinaryExceptionClassifier(map, true, true);
173+
}
174+
156175
/**
157176
* Set a supplier for the number of attempts before retries are exhausted. Includes
158177
* the initial attempt before the retries begin so, generally, will be {@code >= 1}.
@@ -190,7 +209,14 @@ public int getMaxAttempts() {
190209
@Override
191210
public boolean canRetry(RetryContext context) {
192211
Throwable t = context.getLastThrowable();
193-
return (t == null || retryForException(t)) && context.getRetryCount() < getMaxAttempts();
212+
boolean can = (t == null || retryForException(t)) && context.getRetryCount() < getMaxAttempts();
213+
if (!can && !this.recoverableClassifier.classify(t)) {
214+
context.setAttribute(RetryContext.NO_RECOVERY, true);
215+
}
216+
else {
217+
context.removeAttribute(RetryContext.NO_RECOVERY);
218+
}
219+
return can;
194220
}
195221

196222
/**

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -537,20 +537,27 @@ protected <T> T handleRetryExhausted(RecoveryCallback<T> recoveryCallback, Retry
537537
if (state != null && !context.hasAttribute(GLOBAL_STATE)) {
538538
this.retryContextCache.remove(state.getKey());
539539
}
540+
boolean doRecover = !Boolean.TRUE.equals(context.getAttribute(RetryContext.NO_RECOVERY));
540541
if (recoveryCallback != null) {
541-
T recovered = recoveryCallback.recover(context);
542-
context.setAttribute(RetryContext.RECOVERED, true);
543-
return recovered;
542+
if (doRecover) {
543+
T recovered = recoveryCallback.recover(context);
544+
context.setAttribute(RetryContext.RECOVERED, true);
545+
return recovered;
546+
}
547+
else {
548+
logger.debug("Retry exhausted and recovery disabled for this throwable");
549+
}
544550
}
545551
if (state != null) {
546552
this.logger.debug("Retry exhausted after last attempt with no recovery path.");
547-
rethrow(context, "Retry exhausted after last attempt with no recovery path");
553+
rethrow(context, "Retry exhausted after last attempt with no recovery path",
554+
this.throwLastExceptionOnExhausted || !doRecover);
548555
}
549556
throw wrapIfNecessary(context.getLastThrowable());
550557
}
551558

552-
protected <E extends Throwable> void rethrow(RetryContext context, String message) throws E {
553-
if (this.throwLastExceptionOnExhausted) {
559+
protected <E extends Throwable> void rethrow(RetryContext context, String message, boolean wrap) throws E {
560+
if (wrap) {
554561
@SuppressWarnings("unchecked")
555562
E rethrow = (E) context.getLastThrowable();
556563
throw rethrow;

0 commit comments

Comments
 (0)