Skip to content

Commit 10cb191

Browse files
AnaghaSasikumariluwatar
authored andcommitted
Retry exponential backoff iluwatar#775 (iluwatar#829)
* Spatial partition * Retry with exponential backoff * retry exponential backoff * branch error
1 parent a6749cb commit 10cb191

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed

retry/src/main/java/com/iluwatar/retry/App.java

+16
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public static void main(String[] args) throws Exception {
7171
noErrors();
7272
errorNoRetry();
7373
errorWithRetry();
74+
errorWithRetryExponentialBackoff();
7475
}
7576

7677
private static void noErrors() throws Exception {
@@ -102,4 +103,19 @@ private static void errorWithRetry() throws Exception {
102103
+ "the result %s after a number of attempts %s", customerId, retry.attempts()
103104
));
104105
}
106+
107+
private static void errorWithRetryExponentialBackoff() throws Exception {
108+
final RetryExponentialBackoff<String> retry = new RetryExponentialBackoff<>(
109+
new FindCustomer("123", new CustomerNotFoundException("not found")),
110+
6, //6 attempts
111+
30000, //30 s max delay between attempts
112+
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass())
113+
);
114+
op = retry;
115+
final String customerId = op.perform();
116+
LOG.info(String.format(
117+
"However, retrying the operation while ignoring a recoverable error will eventually yield "
118+
+ "the result %s after a number of attempts %s", customerId, retry.attempts()
119+
));
120+
}
105121
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2014-2016 Ilkka Seppälä
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package com.iluwatar.retry;
26+
27+
import java.util.ArrayList;
28+
import java.util.Arrays;
29+
import java.util.Collections;
30+
import java.util.List;
31+
import java.util.Random;
32+
import java.util.concurrent.atomic.AtomicInteger;
33+
import java.util.function.Predicate;
34+
35+
/**
36+
* Decorates {@link BusinessOperation business operation} with "retry" capabilities.
37+
*
38+
* @author George Aristy (george.aristy@gmail.com)
39+
* @param <T> the remote op's return type
40+
*/
41+
public final class RetryExponentialBackoff<T> implements BusinessOperation<T> {
42+
private final BusinessOperation<T> op;
43+
private final int maxAttempts;
44+
private final long maxDelay;
45+
private final AtomicInteger attempts;
46+
private final Predicate<Exception> test;
47+
private final List<Exception> errors;
48+
49+
/**
50+
* Ctor.
51+
*
52+
* @param op the {@link BusinessOperation} to retry
53+
* @param maxAttempts number of times to retry
54+
* @param ignoreTests tests to check whether the remote exception can be ignored. No exceptions
55+
* will be ignored if no tests are given
56+
*/
57+
@SafeVarargs
58+
public RetryExponentialBackoff(
59+
BusinessOperation<T> op,
60+
int maxAttempts,
61+
long maxDelay,
62+
Predicate<Exception>... ignoreTests
63+
) {
64+
this.op = op;
65+
this.maxAttempts = maxAttempts;
66+
this.maxDelay = maxDelay;
67+
this.attempts = new AtomicInteger();
68+
this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false);
69+
this.errors = new ArrayList<>();
70+
}
71+
72+
/**
73+
* The errors encountered while retrying, in the encounter order.
74+
*
75+
* @return the errors encountered while retrying
76+
*/
77+
public List<Exception> errors() {
78+
return Collections.unmodifiableList(this.errors);
79+
}
80+
81+
/**
82+
* The number of retries performed.
83+
*
84+
* @return the number of retries performed
85+
*/
86+
public int attempts() {
87+
return this.attempts.intValue();
88+
}
89+
90+
@Override
91+
public T perform() throws BusinessException {
92+
do {
93+
try {
94+
return this.op.perform();
95+
} catch (BusinessException e) {
96+
this.errors.add(e);
97+
98+
if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) {
99+
throw e;
100+
}
101+
102+
try {
103+
Random rand = new Random();
104+
long testDelay = (long) Math.pow(2, this.attempts()) * 1000 + rand.nextInt(1000);
105+
long delay = testDelay < this.maxDelay ? testDelay : maxDelay;
106+
Thread.sleep(delay);
107+
} catch (InterruptedException f) {
108+
//ignore
109+
}
110+
}
111+
}
112+
while (true);
113+
}
114+
}
115+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2014-2016 Ilkka Seppälä
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package com.iluwatar.retry;
26+
27+
import org.junit.jupiter.api.Test;
28+
import static org.hamcrest.CoreMatchers.hasItem;
29+
import static org.hamcrest.CoreMatchers.is;
30+
import static org.hamcrest.MatcherAssert.assertThat;
31+
32+
/**
33+
* Unit tests for {@link Retry}.
34+
*
35+
* @author George Aristy (george.aristy@gmail.com)
36+
*/
37+
public class RetryExponentialBackoffTest {
38+
/**
39+
* Should contain all errors thrown.
40+
*/
41+
@Test
42+
public void errors() throws Exception {
43+
final BusinessException e = new BusinessException("unhandled");
44+
final RetryExponentialBackoff<String> retry = new RetryExponentialBackoff<>(
45+
() -> {
46+
throw e;
47+
},
48+
2,
49+
0
50+
);
51+
try {
52+
retry.perform();
53+
} catch (BusinessException ex) {
54+
//ignore
55+
}
56+
57+
assertThat(
58+
retry.errors(),
59+
hasItem(e)
60+
);
61+
}
62+
63+
/**
64+
* No exceptions will be ignored, hence final number of attempts should be 1 even if we're asking
65+
* it to attempt twice.
66+
*/
67+
@Test
68+
public void attempts() {
69+
final BusinessException e = new BusinessException("unhandled");
70+
final RetryExponentialBackoff<String> retry = new RetryExponentialBackoff<>(
71+
() -> {
72+
throw e;
73+
},
74+
2,
75+
0
76+
);
77+
try {
78+
retry.perform();
79+
} catch (BusinessException ex) {
80+
//ignore
81+
}
82+
83+
assertThat(
84+
retry.attempts(),
85+
is(1)
86+
);
87+
}
88+
89+
/**
90+
* Final number of attempts should be equal to the number of attempts asked because we are
91+
* asking it to ignore the exception that will be thrown.
92+
*/
93+
@Test
94+
public void ignore() throws Exception {
95+
final BusinessException e = new CustomerNotFoundException("customer not found");
96+
final RetryExponentialBackoff<String> retry = new RetryExponentialBackoff<>(
97+
() -> {
98+
throw e;
99+
},
100+
2,
101+
0,
102+
ex -> CustomerNotFoundException.class.isAssignableFrom(ex.getClass())
103+
);
104+
try {
105+
retry.perform();
106+
} catch (BusinessException ex) {
107+
//ignore
108+
}
109+
110+
assertThat(
111+
retry.attempts(),
112+
is(2)
113+
);
114+
}
115+
}

0 commit comments

Comments
 (0)