Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 0eb3409

Browse files
authored
Merge pull request #74 from raghavan20/match-against-exception-cause
Supporting to match against exception's cause
2 parents bb6dac1 + 9217c10 commit 0eb3409

File tree

7 files changed

+136
-30
lines changed

7 files changed

+136
-30
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ RetryConfig config = new RetryConfigBuilder()
149149
.build();
150150
```
151151

152+
If you want the executor to continue to retry when encountered exception's cause is among a list of exceptions, then specify **retryOnCausedBy()** config option
153+
```java
154+
RetryConfig config = new RetryConfigBuilder()
155+
.retryOnCausedBy()
156+
.retryOnSpecificExceptions(ConnectException.class, TimeoutException.class)
157+
.build();
158+
```
159+
152160
If you want the executor to continue to retry on all encountered exceptions EXCEPT for a few specific ones, specify this using the **retryOnAnyExceptionExcluding()** config option. If this exception strategy is chosen, only the exceptions specified or their subclasses will interupt the executor and throw an **UnexpectedException**.
153161
154162
```java

src/main/java/com/evanlennick/retry4j/CallExecutor.java

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import java.time.Duration;
1212
import java.time.temporal.ChronoUnit;
13-
import java.util.UUID;
13+
import java.util.*;
1414
import java.util.concurrent.Callable;
1515
import java.util.concurrent.TimeUnit;
1616

@@ -178,7 +178,8 @@ private void sleep(long millis, int tries) {
178178
logger.trace("Retry4j executor sleeping for {} ms", millisToSleep);
179179
try {
180180
TimeUnit.MILLISECONDS.sleep(millisToSleep);
181-
} catch (InterruptedException ignored) {}
181+
} catch (InterruptedException ignored) {
182+
}
182183
}
183184

184185
private boolean shouldThrowException(Exception e) {
@@ -191,25 +192,45 @@ private boolean shouldThrowException(Exception e) {
191192
return false;
192193
}
193194

194-
//config says to retry only on specific exceptions
195-
for (Class<? extends Exception> exceptionToRetryOn : this.config.getRetryOnSpecificExceptions()) {
196-
if (exceptionToRetryOn.isAssignableFrom(e.getClass())) {
197-
return false;
198-
}
195+
Set<Class<?>> exceptionsToMatch = new HashSet<>();
196+
exceptionsToMatch.add(e.getClass());
197+
if (this.config.shouldRetryOnCausedBy()) {
198+
exceptionsToMatch.clear();
199+
exceptionsToMatch.addAll(getExceptionCauses(e));
199200
}
200201

201-
//config says to retry on all except specific exceptions
202-
if (!this.config.getRetryOnAnyExceptionExcluding().isEmpty()) {
203-
for (Class<? extends Exception> exceptionToNotRetryOn : this.config.getRetryOnAnyExceptionExcluding()) {
204-
if (exceptionToNotRetryOn.isAssignableFrom(e.getClass())) {
205-
return true;
206-
}
207-
}
208-
return false;
202+
return !exceptionsToMatch.stream().anyMatch(ex -> matchesException(ex));
203+
}
204+
}
205+
206+
private boolean matchesException(Class<?> thrownExceptionClass) {
207+
//config says to retry only on specific exceptions
208+
for (Class<? extends Exception> exceptionToRetryOn : this.config.getRetryOnSpecificExceptions()) {
209+
if (exceptionToRetryOn.isAssignableFrom(thrownExceptionClass)) {
210+
return true;
209211
}
212+
}
210213

214+
//config says to retry on all except specific exceptions
215+
if (!this.config.getRetryOnAnyExceptionExcluding().isEmpty()) {
216+
for (Class<? extends Exception> exceptionToNotRetryOn : this.config.getRetryOnAnyExceptionExcluding()) {
217+
if (exceptionToNotRetryOn.isAssignableFrom(thrownExceptionClass)) {
218+
return false;
219+
}
220+
}
211221
return true;
212222
}
223+
return false;
224+
}
225+
226+
private Set<Class<?>> getExceptionCauses(Exception exception) {
227+
Throwable parent = exception;
228+
Set<Class<?>> causes = new HashSet<>();
229+
while (parent.getCause() != null) {
230+
causes.add(parent.getCause().getClass());
231+
parent = parent.getCause();
232+
}
233+
return causes;
213234
}
214235

215236
@Override

src/main/java/com/evanlennick/retry4j/config/RetryConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class RetryConfig {
1818
private Object valueToRetryOn;
1919
private Boolean retryOnValue = false;
2020
private Function<Exception, Boolean> customRetryOnLogic;
21+
private boolean retryOnCausedBy;
2122

2223
public Object getValueToRetryOn() {
2324
return valueToRetryOn;
@@ -59,6 +60,14 @@ public void setRetryOnAnyExceptionExcluding(Set<Class<? extends Exception>> retr
5960
this.retryOnAnyExceptionExcluding = retryOnAnyExceptionExcluding;
6061
}
6162

63+
public void setRetryOnCausedBy(boolean retryOnCausedBy){
64+
this.retryOnCausedBy = retryOnCausedBy;
65+
}
66+
67+
public boolean shouldRetryOnCausedBy() {
68+
return retryOnCausedBy;
69+
}
70+
6271
public Integer getMaxNumberOfTries() {
6372
return maxNumberOfTries;
6473
}

src/main/java/com/evanlennick/retry4j/config/RetryConfigBuilder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class RetryConfigBuilder {
2323
private boolean builtInExceptionStrategySpecified;
2424
private RetryConfig config;
2525
private boolean validationEnabled;
26+
private boolean matchAgainstExceptionCause;
2627

2728
public final static String MUST_SPECIFY_BACKOFF__ERROR_MSG
2829
= "Retry config must specify a backoff strategy!";
@@ -77,6 +78,11 @@ public RetryConfigBuilder failOnAnyException() {
7778
return this;
7879
}
7980

81+
public RetryConfigBuilder retryOnCausedBy(){
82+
config.setRetryOnCausedBy(true);
83+
return this;
84+
}
85+
8086
@SafeVarargs
8187
public final RetryConfigBuilder retryOnSpecificExceptions(Class<? extends Exception>... exceptions) {
8288
validateExceptionStrategyAddition();

src/test/java/com/evanlennick/retry4j/CallExecutorTest.java

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,63 @@ public void verifyExceptionFromCallThrowsCallFailureException() throws Exception
6868
new CallExecutor(retryConfig).execute(callable);
6969
}
7070

71+
@Test(expectedExceptions = {RetriesExhaustedException.class})
72+
public void shouldMatchExceptionCauseAndRetry() throws Exception {
73+
Callable<Boolean> callable = () -> {
74+
throw new Exception(new CustomTestException("message", 3));
75+
};
76+
77+
RetryConfig retryConfig = retryConfigBuilder
78+
.retryOnCausedBy()
79+
.retryOnSpecificExceptions(CustomTestException.class)
80+
.withMaxNumberOfTries(1)
81+
.withDelayBetweenTries(0, ChronoUnit.SECONDS)
82+
.withFixedBackoff()
83+
.build();
84+
85+
new CallExecutor(retryConfig).execute(callable);
86+
}
87+
88+
89+
@Test(expectedExceptions = {RetriesExhaustedException.class})
90+
public void shouldMatchExceptionCauseAtGreaterThanALevelDeepAndRetry() throws Exception {
91+
class CustomException extends Exception {
92+
CustomException(Throwable cause){
93+
super(cause);
94+
}
95+
}
96+
Callable<Boolean> callable = () -> {
97+
throw new Exception(new CustomException(new RuntimeException(new IOException())));
98+
};
99+
100+
RetryConfig retryConfig = retryConfigBuilder
101+
.retryOnCausedBy()
102+
.retryOnSpecificExceptions(IOException.class)
103+
.withMaxNumberOfTries(1)
104+
.withDelayBetweenTries(0, ChronoUnit.SECONDS)
105+
.withFixedBackoff()
106+
.build();
107+
108+
new CallExecutor(retryConfig).execute(callable);
109+
}
110+
111+
@Test(expectedExceptions = {UnexpectedException.class})
112+
public void shouldThrowUnexpectedIfThrownExceptionCauseDoesNotMatchRetryExceptions() throws Exception {
113+
Callable<Boolean> callable = () -> {
114+
throw new Exception(new CustomTestException("message", 3));
115+
};
116+
117+
RetryConfig retryConfig = retryConfigBuilder
118+
.retryOnCausedBy()
119+
.retryOnSpecificExceptions(IOException.class)
120+
.withMaxNumberOfTries(1)
121+
.withDelayBetweenTries(0, ChronoUnit.SECONDS)
122+
.withFixedBackoff()
123+
.build();
124+
125+
new CallExecutor(retryConfig).execute(callable);
126+
}
127+
71128
@Test(expectedExceptions = {UnexpectedException.class})
72129
public void verifySpecificSuperclassExceptionThrowsUnexpectedException() throws Exception {
73130
Callable<Boolean> callable = () -> {
@@ -153,7 +210,9 @@ public void verifyStatusIsPopulatedOnSuccessfulCall() throws Exception {
153210

154211
@Test
155212
public void verifyStatusIsPopulatedOnFailedCall() throws Exception {
156-
Callable<Boolean> callable = () -> { throw new FileNotFoundException(); };
213+
Callable<Boolean> callable = () -> {
214+
throw new FileNotFoundException();
215+
};
157216

158217
RetryConfig retryConfig = retryConfigBuilder
159218
.withMaxNumberOfTries(5)
@@ -252,7 +311,8 @@ public void verifyRetryPolicyTimeoutIsUsed() {
252311
final long before = System.currentTimeMillis();
253312
try {
254313
new CallExecutor<>(retryConfig).execute(callable);
255-
} catch (RetriesExhaustedException ignored) {}
314+
} catch (RetriesExhaustedException ignored) {
315+
}
256316

257317
assertThat(System.currentTimeMillis() - before).isGreaterThan(5000);
258318
verify(mockBackOffStrategy).getDurationToWait(1, delayBetweenTriesDuration);

src/test/java/com/evanlennick/retry4j/CallExecutorTest_RetryOnCustomLogicTest.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,4 @@ public void verifyShouldNotRetryOnCustomException() {
8383
});
8484
}
8585

86-
private class CustomTestException extends RuntimeException {
87-
88-
private int someValue;
89-
90-
public CustomTestException(String message, int someValue) {
91-
super(message);
92-
this.someValue = someValue;
93-
}
94-
95-
public int getSomeValue() {
96-
return someValue;
97-
}
98-
}
9986
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.evanlennick.retry4j;
2+
3+
class CustomTestException extends RuntimeException {
4+
5+
private int someValue;
6+
7+
public CustomTestException(String message, int someValue) {
8+
super(message);
9+
this.someValue = someValue;
10+
}
11+
12+
public int getSomeValue() {
13+
return someValue;
14+
}
15+
}

0 commit comments

Comments
 (0)