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

Commit 17e9814

Browse files
committed
fixed NPE bug with no wait backoff and added new capability to specify custom exception checking logic
1 parent 4a1ce62 commit 17e9814

File tree

7 files changed

+184
-27
lines changed

7 files changed

+184
-27
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ Retry4j does not require any external dependencies. It does require that you are
127127

128128
If you do not specify how exceptions should be handled or explicitly say **failOnAnyException()**, the CallExecutor will fail and throw an **UnexpectedException** when encountering exceptions while running. Use this configuration if you want the executor to cease its work when it runs into any exception at all.
129129

130-
131130
```java
132131
RetryConfig config = new RetryConfigBuilder()
133132
.failOnAnyException()
@@ -144,7 +143,6 @@ RetryConfig config = new RetryConfigBuilder()
144143

145144
If you want the executor to continue to retry on all encountered exceptions, specify this using the **retryOnAnyException()** config option.
146145

147-
148146
```java
149147
RetryConfig config = new RetryConfigBuilder()
150148
.retryOnAnyException()
@@ -159,6 +157,18 @@ RetryConfig config = new RetryConfigBuilder()
159157
.build();
160158
```
161159

160+
If you do not want to use these built-in mechanisms for retrying on exceptions, you can override them and create custom logic:
161+
162+
```java
163+
RetryConfig config = new RetryConfigBuilder()
164+
.retryOnCustomExceptionLogic(ex -> {
165+
//return true to retry, otherwise return false
166+
})
167+
.build();
168+
```
169+
170+
If you create custom exception logic, no other built-in configuration can be used at the same time.
171+
162172
### Value Handling Config
163173

164174
If you want the executor to retry based on the returned value from the Callable:

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

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import java.util.concurrent.TimeUnit;
1616

1717
/**
18-
* Default implementation that does a single, synchrnous retry in the same thread that it is called from.
18+
* Default implementation that does a single, synchronous retry in the same thread that it is called from.
1919
*
2020
* @param <T> The type that is returned by the Callable (eg: Boolean, Void, Object, etc)
2121
*/
@@ -62,7 +62,8 @@ public Status<T> execute(Callable<T> callable, String callName) {
6262
status.setStartTime(start);
6363

6464
int maxTries = config.getMaxNumberOfTries();
65-
long millisBetweenTries = config.getDelayBetweenRetries().toMillis();
65+
long millisBetweenTries = config.getDelayBetweenRetries() != null
66+
? config.getDelayBetweenRetries().toMillis() : 0L;
6667
this.status.setCallName(callName);
6768

6869
AttemptStatus<T> attemptStatus = new AttemptStatus<>();
@@ -181,26 +182,31 @@ private void sleep(long millis, int tries) {
181182
}
182183

183184
private boolean shouldThrowException(Exception e) {
184-
//config says to always retry
185-
if (this.config.isRetryOnAnyException()) {
186-
return false;
187-
}
188-
189-
//config says to retry only on specific exceptions
190-
for (Class<? extends Exception> exceptionInSet : this.config.getRetryOnSpecificExceptions()) {
191-
if (exceptionInSet.isAssignableFrom(e.getClass())) {
185+
if (this.config.getCustomRetryOnLogic() != null) {
186+
//custom retry logic
187+
return !this.config.getCustomRetryOnLogic().apply(e);
188+
} else {
189+
//config says to always retry
190+
if (this.config.isRetryOnAnyException()) {
192191
return false;
193192
}
194-
}
195193

196-
//config says to retry on all except specific exceptions
197-
for (Class<? extends Exception> exceptionInSet : this.config.getRetryOnAnyExceptionExcluding()) {
198-
if (!exceptionInSet.isAssignableFrom(e.getClass())) {
199-
return false;
194+
//config says to retry only on specific exceptions
195+
for (Class<? extends Exception> exceptionInSet : this.config.getRetryOnSpecificExceptions()) {
196+
if (exceptionInSet.isAssignableFrom(e.getClass())) {
197+
return false;
198+
}
200199
}
201-
}
202200

203-
return true;
201+
//config says to retry on all except specific exceptions
202+
for (Class<? extends Exception> exceptionInSet : this.config.getRetryOnAnyExceptionExcluding()) {
203+
if (!exceptionInSet.isAssignableFrom(e.getClass())) {
204+
return false;
205+
}
206+
}
207+
208+
return true;
209+
}
204210
}
205211

206212
@Override

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.time.Duration;
66
import java.util.HashSet;
77
import java.util.Set;
8+
import java.util.function.Function;
89

910
public class RetryConfig {
1011

@@ -16,6 +17,7 @@ public class RetryConfig {
1617
private BackoffStrategy backoffStrategy;
1718
private Object valueToRetryOn;
1819
private Boolean retryOnValue = false;
20+
private Function<Exception, Boolean> customRetryOnLogic;
1921

2022
public Object getValueToRetryOn() {
2123
return valueToRetryOn;
@@ -89,14 +91,26 @@ public void setBackoffStrategy(BackoffStrategy backoffStrategy) {
8991
this.backoffStrategy = backoffStrategy;
9092
}
9193

94+
public Function<Exception, Boolean> getCustomRetryOnLogic() {
95+
return customRetryOnLogic;
96+
}
97+
98+
public void setCustomRetryOnLogic(Function<Exception, Boolean> customRetryOnLogic) {
99+
this.customRetryOnLogic = customRetryOnLogic;
100+
}
101+
92102
@Override
93103
public String toString() {
94104
final StringBuilder sb = new StringBuilder("RetryConfig{");
95105
sb.append("retryOnAnyException=").append(retryOnAnyException);
96106
sb.append(", retryOnSpecificExceptions=").append(retryOnSpecificExceptions);
107+
sb.append(", retryOnAnyExceptionExcluding=").append(retryOnAnyExceptionExcluding);
97108
sb.append(", maxNumberOfTries=").append(maxNumberOfTries);
98109
sb.append(", delayBetweenRetries=").append(delayBetweenRetries);
99110
sb.append(", backoffStrategy=").append(backoffStrategy);
111+
sb.append(", valueToRetryOn=").append(valueToRetryOn);
112+
sb.append(", retryOnValue=").append(retryOnValue);
113+
sb.append(", customRetryOnLogic=").append(customRetryOnLogic);
100114
sb.append('}');
101115
return sb.toString();
102116
}

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
import java.util.Arrays;
1515
import java.util.HashSet;
1616
import java.util.Set;
17+
import java.util.function.Function;
1718

1819
import static java.time.temporal.ChronoUnit.SECONDS;
1920

2021
public class RetryConfigBuilder {
2122

22-
private boolean exceptionStrategySpecified;
23+
private boolean builtInExceptionStrategySpecified;
2324
private RetryConfig config;
2425
private boolean validationEnabled;
2526

@@ -33,10 +34,12 @@ public class RetryConfigBuilder {
3334
= "Retry config cannot specify more than one exception strategy!";
3435
public final static String ALREADY_SPECIFIED_NUMBER_OF_TRIES__ERROR_MSG
3536
= "Number of tries can only be specified once!";
37+
public final static String CAN_ONLY_SPECIFY_CUSTOM_EXCEPTION_STRAT__ERROR_MSG
38+
= "You cannot use built in exception logic and custom exception logic in the same config!";
3639

3740
public RetryConfigBuilder() {
3841
this.config = new RetryConfig();
39-
this.exceptionStrategySpecified = false;
42+
this.builtInExceptionStrategySpecified = false;
4043
this.validationEnabled = true;
4144
}
4245

@@ -58,7 +61,7 @@ public RetryConfigBuilder retryOnAnyException() {
5861

5962
config.setRetryOnAnyException(true);
6063

61-
exceptionStrategySpecified = true;
64+
builtInExceptionStrategySpecified = true;
6265
return this;
6366
}
6467

@@ -68,7 +71,7 @@ public RetryConfigBuilder failOnAnyException() {
6871
config.setRetryOnAnyException(false);
6972
config.setRetryOnSpecificExceptions(new HashSet<>());
7073

71-
exceptionStrategySpecified = true;
74+
builtInExceptionStrategySpecified = true;
7275
return this;
7376
}
7477

@@ -79,7 +82,7 @@ public final RetryConfigBuilder retryOnSpecificExceptions(Class<? extends Except
7982
Set<Class<? extends Exception>> setOfExceptions = new HashSet<>(Arrays.asList(exceptions));
8083
config.setRetryOnSpecificExceptions(setOfExceptions);
8184

82-
exceptionStrategySpecified = true;
85+
builtInExceptionStrategySpecified = true;
8386
return this;
8487
}
8588

@@ -90,7 +93,7 @@ public final RetryConfigBuilder retryOnAnyExceptionExcluding(Class<? extends Exc
9093
Set<Class<? extends Exception>> setOfExceptions = new HashSet<>(Arrays.asList(exceptions));
9194
config.setRetryOnAnyExceptionExcluding(setOfExceptions);
9295

93-
exceptionStrategySpecified = true;
96+
builtInExceptionStrategySpecified = true;
9497
return this;
9598
}
9699

@@ -101,6 +104,11 @@ public final RetryConfigBuilder retryOnReturnValue(Object value) {
101104
return this;
102105
}
103106

107+
public RetryConfigBuilder retryOnCustomExceptionLogic(Function<Exception, Boolean> customRetryFunction) {
108+
this.config.setCustomRetryOnLogic(customRetryFunction);
109+
return this;
110+
}
111+
104112
public RetryConfigBuilder withMaxNumberOfTries(int max) {
105113
if (config.getMaxNumberOfTries() != null) {
106114
throw new InvalidRetryConfigException(ALREADY_SPECIFIED_NUMBER_OF_TRIES__ERROR_MSG);
@@ -190,6 +198,10 @@ private void validateConfig() {
190198
throw new InvalidRetryConfigException(MUST_SPECIFY_MAX_TRIES__ERROR_MSG);
191199
}
192200

201+
if (null != config.getCustomRetryOnLogic() && builtInExceptionStrategySpecified) {
202+
throw new InvalidRetryConfigException(CAN_ONLY_SPECIFY_CUSTOM_EXCEPTION_STRAT__ERROR_MSG);
203+
}
204+
193205
config.getBackoffStrategy().validateConfig(config);
194206
}
195207

@@ -208,7 +220,7 @@ private void validateExceptionStrategyAddition() {
208220
return;
209221
}
210222

211-
if (exceptionStrategySpecified) {
223+
if (builtInExceptionStrategySpecified) {
212224
throw new InvalidRetryConfigException(CAN_ONLY_SPECIFY_ONE_EXCEPTION_STRAT__ERROR_MSG);
213225
}
214226
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.evanlennick.retry4j.config.RetryConfigBuilder;
66
import com.evanlennick.retry4j.exception.RetriesExhaustedException;
77
import com.evanlennick.retry4j.exception.UnexpectedException;
8-
98
import org.mockito.Mock;
109
import org.mockito.MockitoAnnotations;
1110
import org.testng.annotations.BeforeMethod;
@@ -258,4 +257,18 @@ public void verifyRetryPolicyTimeoutIsUsed() {
258257
assertThat(System.currentTimeMillis() - before).isGreaterThan(5000);
259258
verify(mockBackOffStrategy).getDurationToWait(1, delayBetweenTriesDuration);
260259
}
260+
261+
@Test
262+
public void verifyNoDurationSpecifiedSucceeds() {
263+
Callable<String> callable = () -> "test";
264+
265+
RetryConfig noWaitConfig = new RetryConfigBuilder()
266+
.withMaxNumberOfTries(1)
267+
.withNoWaitBackoff()
268+
.build();
269+
270+
Status status = new CallExecutor(noWaitConfig).execute(callable);
271+
272+
assertThat(status.getResult()).isEqualTo("test");
273+
}
261274
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.evanlennick.retry4j;
2+
3+
import com.evanlennick.retry4j.config.RetryConfig;
4+
import com.evanlennick.retry4j.config.RetryConfigBuilder;
5+
import com.evanlennick.retry4j.exception.RetriesExhaustedException;
6+
import com.evanlennick.retry4j.exception.UnexpectedException;
7+
import org.junit.BeforeClass;
8+
import org.junit.Test;
9+
10+
import java.time.Duration;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.testng.Assert.fail;
14+
15+
public class CallExecutorTest_RetryOnCustomLogicTest {
16+
17+
private static RetryConfig config;
18+
19+
@BeforeClass
20+
public static void setup() {
21+
config = new RetryConfigBuilder()
22+
.retryOnCustomExceptionLogic(ex -> ex.getMessage().contains("should retry!"))
23+
.withFixedBackoff()
24+
.withDelayBetweenTries(Duration.ofMillis(1))
25+
.withMaxNumberOfTries(3)
26+
.build();
27+
}
28+
29+
@Test
30+
public void verifyShouldRetry() {
31+
try {
32+
new CallExecutor<>(config)
33+
.execute(() -> {
34+
throw new RuntimeException("should retry!");
35+
});
36+
fail();
37+
} catch (RetriesExhaustedException e) {
38+
assertThat(e.getStatus().getTotalTries()).isEqualTo(3);
39+
}
40+
}
41+
42+
@Test(expected = UnexpectedException.class)
43+
public void verifyShouldNotRetry() {
44+
new CallExecutor<>(config)
45+
.execute(() -> {
46+
throw new RuntimeException("should NOT retry!");
47+
});
48+
}
49+
}

src/test/java/com/evanlennick/retry4j/config/RetryConfigBuilderTest_WithValidationTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.evanlennick.retry4j.config;
22

33
import com.evanlennick.retry4j.exception.InvalidRetryConfigException;
4+
import org.testng.TestException;
45
import org.testng.annotations.BeforeMethod;
56
import org.testng.annotations.Test;
67

78
import java.net.ConnectException;
9+
import java.time.Duration;
810
import java.time.temporal.ChronoUnit;
911

1012
import static org.assertj.core.api.Assertions.assertThat;
@@ -193,4 +195,55 @@ public void verifyMaxRetriesSpecifiedTwiceThrowsException_twoNumbers() {
193195
.isEqualTo(RetryConfigBuilder.ALREADY_SPECIFIED_NUMBER_OF_TRIES__ERROR_MSG);
194196
}
195197
}
198+
199+
@Test
200+
public void verifySpecifyingMultipleBuildInAndCustomExceptionStrategiesThrowsException_anyException() {
201+
try {
202+
retryConfigBuilder
203+
.retryOnAnyException()
204+
.retryOnCustomExceptionLogic(ex -> ex.getMessage().contains("should retry!"))
205+
.withFixedBackoff()
206+
.withDelayBetweenTries(Duration.ofMillis(1))
207+
.withMaxNumberOfTries(3)
208+
.build();
209+
fail("Expected InvalidRetryConfigException but one wasn't thrown!");
210+
} catch (InvalidRetryConfigException e) {
211+
assertThat(e.getMessage())
212+
.isEqualTo(RetryConfigBuilder.CAN_ONLY_SPECIFY_CUSTOM_EXCEPTION_STRAT__ERROR_MSG);
213+
}
214+
}
215+
216+
@Test
217+
public void verifySpecifyingMultipleBuildInAndCustomExceptionStrategiesThrowsException_specificExceptions() {
218+
try {
219+
retryConfigBuilder
220+
.retryOnSpecificExceptions(TestException.class)
221+
.retryOnCustomExceptionLogic(ex -> ex.getMessage().contains("should retry!"))
222+
.withFixedBackoff()
223+
.withDelayBetweenTries(Duration.ofMillis(1))
224+
.withMaxNumberOfTries(3)
225+
.build();
226+
fail("Expected InvalidRetryConfigException but one wasn't thrown!");
227+
} catch (InvalidRetryConfigException e) {
228+
assertThat(e.getMessage())
229+
.isEqualTo(RetryConfigBuilder.CAN_ONLY_SPECIFY_CUSTOM_EXCEPTION_STRAT__ERROR_MSG);
230+
}
231+
}
232+
233+
@Test
234+
public void verifySpecifyingMultipleBuildInAndCustomExceptionStrategiesThrowsException_excludingExceptions() {
235+
try {
236+
retryConfigBuilder
237+
.retryOnAnyExceptionExcluding(TestException.class)
238+
.retryOnCustomExceptionLogic(ex -> ex.getMessage().contains("should retry!"))
239+
.withFixedBackoff()
240+
.withDelayBetweenTries(Duration.ofMillis(1))
241+
.withMaxNumberOfTries(3)
242+
.build();
243+
fail("Expected InvalidRetryConfigException but one wasn't thrown!");
244+
} catch (InvalidRetryConfigException e) {
245+
assertThat(e.getMessage())
246+
.isEqualTo(RetryConfigBuilder.CAN_ONLY_SPECIFY_CUSTOM_EXCEPTION_STRAT__ERROR_MSG);
247+
}
248+
}
196249
}

0 commit comments

Comments
 (0)