Skip to content

Commit 462518f

Browse files
author
Robert Winkler
committed
Added the feature to retry a failed function
1 parent 0f030e1 commit 462518f

File tree

12 files changed

+451
-41
lines changed

12 files changed

+451
-41
lines changed

README.adoc

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
= CircuitBreaker for Java8
22
:author: Robert Winkler
3-
:version: 0.1.3
3+
:version: 0.1.4
44
:hardbreaks:
55

66
image:https://travis-ci.org/RobWin/circuitbreaker-java8.svg?branch=master["Build Status", link="https://travis-ci.org/RobWin/circuitbreaker-java8"] image:https://coveralls.io/repos/RobWin/circuitbreaker-java8/badge.svg["Coverage Status", link="https://coveralls.io/r/RobWin/circuitbreaker-java8"] image:https://api.bintray.com/packages/robwin/maven/circuitbreaker-java8/images/download.svg[link="https://bintray.com/robwin/maven/circuitbreaker-java8/_latestVersion"] image:http://img.shields.io/badge/license-ASF2-blue.svg["Apache License 2", link="http://www.apache.org/licenses/LICENSE-2.0.txt"]
77

88
This library is an easy-to-use implementation of the http://martinfowler.com/bliki/CircuitBreaker.html[CircuitBreaker pattern] optimized for Java 8 and functional programming in a multithreaded environment.
9-
The library provides several higher-order functions to decorate any `Supplier / Runnable / Function` or `CheckedSupplier / CheckedRunnable / CheckedFunction` with a Circuit Breaker. In the following I call the higher-order functions `decorators`. The decorators return an enhanced version of your function. Furthermore, the library provides decorators to measure runtime metrics of your functions by using https://dropwizard.github.io/metrics/[Dropwizard Metrics]. You can stack more than one decorator on any given function. That means, you can combine a Metrics decorator with a CircuitBreaker decorator. Any decorated function can be invoked synchronously or asynchronously.
9+
The library provides several higher-order functions to decorate any `Supplier / Runnable / Function` or `CheckedSupplier / CheckedRunnable / CheckedFunction` with a Circuit Breaker. In the following I call the higher-order functions `decorators`. The decorators return an enhanced version of your function. Furthermore, the library provides decorators to measure runtime metrics of your functions by using https://dropwizard.github.io/metrics/[Dropwizard Metrics] and decorators to retry a failed function. You can stack more than one decorator on any given function. That means, you can combine a Metrics decorator with a CircuitBreaker decorator. Any decorated function can be invoked synchronously or asynchronously.
1010
The project should be combined with a functional library for Java 8 like https://github.com/javaslang/javaslang[javaslang]. The project requires at least JDK 8.
1111

1212
The CircuitBreaker is implemented via a finite state machine with three states: `CLOSED`, `OPEN` and `HALF_OPEN`. The CircuitBreaker does not know anything about the backend's state by itself, but uses the information provided by the decorators via `CircuitBreaker::recordSuccess()` and `CircuitBreaker::recordFailure(throwable)`. The decorators are pure functions. The result of a decorator depends solely on the input parameters. See example:
@@ -20,16 +20,16 @@ static <T> Supplier<T> decorateSupplier(Supplier<T> supplier, CircuitBreaker cir
2020
T returnValue = supplier.get();
2121
circuitBreaker.recordSuccess();
2222
return returnValue;
23-
} catch (Throwable throwable) {
24-
circuitBreaker.recordFailure(throwable);
25-
throw throwable;
23+
} catch (Exception exception) {
24+
circuitBreaker.recordFailure(exception);
25+
throw exception;
2626
}
2727
};
2828
}
2929
----
3030

3131
The state of the CircuitBreaker changes from `CLOSED` to `OPEN` if a (configurable) number of call attempts have failed consecutively. Then, all access to the backend is blocked for a (configurable) time interval. `CircuitBreaker::isCallPermitted()` throws a `CircuitBreakerOpenException`, if the CircuitBreaker is `OPEN`.
32-
After the time interval has elapsed, the CircuitBreaker state changes to `HALF_CLOSED` and allows calls to see if the backend is still unavailable or has become available again. On success or failure, the state changes back to `CLOSED` or `OPEN`, respectively. `CircuitBreaker::recordFailure(throwable)` also checks if the type of exception should increase the number of failure attempts.
32+
After the time interval has elapsed, the CircuitBreaker state changes to `HALF_CLOSED` and allows calls to see if the backend is still unavailable or has become available again. On success or failure, the state changes back to `CLOSED` or `OPEN`, respectively. `CircuitBreaker::recordFailure(exception)` also checks if the type of exception should increase the number of failure attempts.
3333

3434
== Usage guide
3535

@@ -189,7 +189,7 @@ assertThat(result.isSuccess()).isTrue();
189189
assertThat(result.get()).isEqualTo("Hello Recovery");
190190
----
191191

192-
=== Example of how to ignore exceptions
192+
=== Ignore exceptions example
193193

194194
The following test cases shows how to add exceptions to the ignore list which must not trigger the CircuitBreaker.
195195

@@ -222,7 +222,39 @@ assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
222222
assertThat(result.failed().get()).isInstanceOf(IOException.class);
223223
----
224224

225-
=== Example with CompletableFuture
225+
=== Retry example
226+
227+
You can also retry a failed function.
228+
229+
[source,java]
230+
----
231+
// Given I have a HelloWorldService which throws an exception
232+
HelloWorldService helloWorldService = mock(HelloWorldService.class);
233+
given(helloWorldService.sayHelloWorld()).willThrow(new WebServiceException("BAM!"));
234+
235+
// Create a Retry with default configuration
236+
// (maxAttempts = 3, waitInterval = 500[ms], ignoredExceptions = empty)
237+
Retry retryContext = Retry.ofDefaults();
238+
// Decorate the invocation of the HelloWorldService
239+
Try.CheckedSupplier<String> retryableSupplier = Retry.retryableCheckedSupplier(helloWorldService::sayHelloWorld, retryContext);
240+
241+
// When I invoke the function
242+
Try<String> result = Try.of(retryableSupplier).recover((throwable) -> "Hello world from recovery function");
243+
244+
// Then the helloWorldService should be invoked 3 times
245+
BDDMockito.then(helloWorldService).should(times(3)).sayHelloWorld();
246+
// and the exception should be handled by the recovery function
247+
assertThat(result.get()).isEqualTo("Hello world from recovery function");
248+
----
249+
250+
As an alternative you can create a custom `RetryContext`. In order to create a custom RetryContext, you can use the RetryContext builder. You can configure the maximum number of retry attempts and the wait interval [ms] between successive attempts. Furthermore, you can add exceptions to the ignore list which must not trigger a retry.
251+
252+
[source,java]
253+
----
254+
Retry retryContext = Retry.custom().maxAttempts(2).ignoredException(WebServiceException.class).build();
255+
----
256+
257+
=== CompletableFuture example
226258

227259
You can also invoke a decorated function asynchronously and chain further functions.
228260

RELEASENOTES.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@
1111

1212
== Version 0.1.3
1313
* Added a static factory method for the CircuitBreakerConfig builder
14+
15+
== Version 0.1.4
16+
* Added the feature to retry a failed function

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ apply from: 'gradle/publishing.gradle'
1818

1919
group = 'io.github.robwin'
2020
sourceCompatibility = 1.8
21-
version = '0.1.3'
21+
version = '0.1.4'
2222
description = 'A CircuitBreaker pattern implementation for Java 8 and functional programming-'
2323

2424
repositories {
@@ -32,6 +32,8 @@ dependencies {
3232
testCompile "org.assertj:assertj-core:3.0.0"
3333
testCompile "ch.qos.logback:logback-classic:0.9.26"
3434
testCompile "io.dropwizard.metrics:metrics-healthchecks:3.1.2"
35+
testCompile "org.mockito:mockito-all:1.10.19"
36+
3537
}
3638

3739
task sourcesJar(type: Jar, dependsOn: classes) {

src/main/java/io/github/robwin/circuitbreaker/CircuitBreaker.java

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ public interface CircuitBreaker {
4444
* Records a backend failure.
4545
* This must be called if a call to a backend fails
4646
*
47-
* @param throwable The throwable which must be recorded
47+
* @param exception The exception which must be recorded
4848
*/
49-
void recordFailure(Throwable throwable);
49+
void recordFailure(Exception exception);
5050

5151
/**
5252
* Records success of a call to a backend.
@@ -90,9 +90,9 @@ static <T> Try.CheckedSupplier<T> decorateCheckedSupplier(Try.CheckedSupplier<T>
9090
T returnValue = supplier.get();
9191
circuitBreaker.recordSuccess();
9292
return returnValue;
93-
} catch (Throwable throwable) {
94-
circuitBreaker.recordFailure(throwable);
95-
throw throwable;
93+
} catch (Exception exception) {
94+
circuitBreaker.recordFailure(exception);
95+
throw exception;
9696
}
9797
};
9898
}
@@ -103,9 +103,9 @@ static Try.CheckedRunnable decorateCheckedRunnable(Try.CheckedRunnable runnable,
103103
try{
104104
runnable.run();
105105
circuitBreaker.recordSuccess();
106-
} catch (Throwable throwable){
107-
circuitBreaker.recordFailure(throwable);
108-
throw throwable;
106+
} catch (Exception exception){
107+
circuitBreaker.recordFailure(exception);
108+
throw exception;
109109
}
110110
};
111111
}
@@ -117,9 +117,9 @@ static <T> Supplier<T> decorateSupplier(Supplier<T> supplier, CircuitBreaker cir
117117
T returnValue = supplier.get();
118118
circuitBreaker.recordSuccess();
119119
return returnValue;
120-
} catch (Throwable throwable) {
121-
circuitBreaker.recordFailure(throwable);
122-
throw throwable;
120+
} catch (Exception exception) {
121+
circuitBreaker.recordFailure(exception);
122+
throw exception;
123123
}
124124
};
125125
}
@@ -130,9 +130,9 @@ static Runnable decorateRunnable(Runnable runnable, CircuitBreaker circuitBreake
130130
try{
131131
runnable.run();
132132
circuitBreaker.recordSuccess();
133-
} catch (Throwable throwable){
134-
circuitBreaker.recordFailure(throwable);
135-
throw throwable;
133+
} catch (Exception exception){
134+
circuitBreaker.recordFailure(exception);
135+
throw exception;
136136
}
137137
};
138138
}
@@ -144,9 +144,9 @@ static <T, R> Function<T, R> decorateFunction(Function<T, R> function, CircuitBr
144144
R returnValue = function.apply(t);
145145
circuitBreaker.recordSuccess();
146146
return returnValue;
147-
} catch (Throwable throwable){
148-
circuitBreaker.recordFailure(throwable);
149-
throw throwable;
147+
} catch (Exception exception){
148+
circuitBreaker.recordFailure(exception);
149+
throw exception;
150150
}
151151
};
152152
}
@@ -158,9 +158,9 @@ static <T, R> Try.CheckedFunction<T, R> decorateCheckedFunction(Try.CheckedFunct
158158
R returnValue = function.apply(t);
159159
circuitBreaker.recordSuccess();
160160
return returnValue;
161-
} catch (Throwable throwable){
162-
circuitBreaker.recordFailure(throwable);
163-
throw throwable;
161+
} catch (Exception exception){
162+
circuitBreaker.recordFailure(exception);
163+
throw exception;
164164
}
165165
};
166166
}

src/main/java/io/github/robwin/circuitbreaker/CircuitBreakerConfig.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@
2222
import java.util.List;
2323

2424
public class CircuitBreakerConfig {
25+
26+
private static final int DEFAULT_MAX_FAILURES = 3;
27+
private static final int DEFAULT_WAIT_INTERVAL = 60000;
28+
2529
// The maximum number of allowed failures
26-
private int maxFailures;
30+
private final int maxFailures;
2731
// The wait interval which specifies how long the CircuitBreaker should stay OPEN
28-
private int waitInterval;
32+
private final int waitInterval;
2933
// Exceptions which do not count as failures and thus not trigger the circuit breaker.
30-
private List<Class<? extends Throwable>> ignoredExceptions;
34+
private final List<Class<? extends Throwable>> ignoredExceptions;
3135

3236
private CircuitBreakerConfig(int maxFailures, int waitInterval, List<Class<? extends Throwable>> ignoredExceptions){
3337
this.maxFailures = maxFailures;
@@ -52,26 +56,38 @@ public static CircuitBreakerConfig.Builder custom(){
5256
}
5357

5458
public static class Builder {
55-
private int maxFailures = 3;
56-
private int waitInterval = 60000;
59+
private int maxFailures = DEFAULT_MAX_FAILURES;
60+
private int waitInterval = DEFAULT_WAIT_INTERVAL;
5761
private List<Class<? extends Throwable>> ignoredExceptions = new ArrayList<>();
5862

5963
public Builder maxFailures(int maxFailures) {
64+
if (maxFailures < 0) {
65+
throw new IllegalArgumentException("maxFailures must be greater than or equal to 0");
66+
}
6067
this.maxFailures = maxFailures;
6168
return this;
6269
}
6370

6471
public Builder waitInterval(int waitInterval) {
72+
if (waitInterval < 100) {
73+
throw new IllegalArgumentException("waitInterval must be at least 100[ms]");
74+
}
6575
this.waitInterval = waitInterval;
6676
return this;
6777
}
6878

6979
public Builder ignoredException(Class<? extends Throwable> ignoredException) {
80+
if (ignoredException == null) {
81+
throw new IllegalArgumentException("ignoredException must not be null");
82+
}
7083
ignoredExceptions.add(ignoredException);
7184
return this;
7285
}
7386

7487
public Builder ignoredExceptions(List<Class<? extends Throwable>> ignoredExceptions) {
88+
if (ignoredExceptions == null) {
89+
throw new IllegalArgumentException("ignoredExceptions must not be null");
90+
}
7591
this.ignoredExceptions = ignoredExceptions;
7692
return this;
7793
}

src/main/java/io/github/robwin/circuitbreaker/CircuitBreakerStateMachine.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
* CircuitBreaker finite state machine.
2626
* This CircuitBreaker is implemented via a (timed) finite state machine. It does not have a way to know anything about the
2727
* backend's state by itself, but uses only the information provided by calls to {@link #recordSuccess()} and
28-
* {@link #recordFailure()}.
28+
* {@link #recordFailure(java.lang.Exception)}.
2929
* The state of the CircuitBreaker changes from `CLOSED` to `OPEN` if a (configurable) number of call attempts have failed consecutively.
3030
* Then, all access to the backend is blocked for a (configurable) time interval. After that, the CircuitBreaker state changes to `HALF_CLOSED` tentatively, to see if the backend is still dead or has become available again.
3131
* On success or failure, the state changes back to `CLOSED` or `OPEN`, respectively.
@@ -66,9 +66,9 @@ public boolean isCallPermitted() {
6666
* Records a failure.
6767
*/
6868
@Override
69-
public void recordFailure(Throwable throwable) {
69+
public void recordFailure(Exception exception) {
7070
if(circuitBreakerConfig.getIgnoredExceptions().stream()
71-
.noneMatch(ignoredException -> ignoredException.isInstance(throwable))){
71+
.noneMatch(ignoredException -> ignoredException.isInstance(exception))){
7272
stateReference.get().recordFailure();
7373
}
7474
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package io.github.robwin.retry;
2+
3+
import javaslang.control.Try;
4+
5+
import java.util.function.Function;
6+
import java.util.function.Supplier;
7+
8+
public interface Retry {
9+
10+
/**
11+
* Checks if the call should be retried
12+
*
13+
* @return the maximum number of attempts
14+
*/
15+
boolean isRetryAllowedAfterException() throws Exception;
16+
17+
/**
18+
* Checks if the call should be retried
19+
*
20+
* @return the maximum number of attempts
21+
*/
22+
boolean isRetryAllowedAfterRuntimeException();
23+
24+
/**
25+
* Handles a checked exception
26+
*/
27+
void handleException(Exception exception) throws Throwable;
28+
29+
/**
30+
* Handles a runtime exception
31+
*/
32+
void handleRuntimeException(RuntimeException runtimeException);
33+
34+
public static RetryContext.Builder custom(){
35+
return new RetryContext.Builder();
36+
}
37+
38+
public static Retry ofDefaults(){
39+
return Retry.custom().build();
40+
}
41+
42+
static <T> Try.CheckedSupplier<T> retryableCheckedSupplier(Try.CheckedSupplier<T> supplier, Retry retryContext){
43+
return () -> {
44+
do try {
45+
return supplier.get();
46+
} catch (Exception exception) {
47+
retryContext.handleException(exception);
48+
} while (retryContext.isRetryAllowedAfterException());
49+
// Should never reach this code
50+
return null;
51+
};
52+
}
53+
54+
static <T> Try.CheckedRunnable retryableCheckedRunnable(Try.CheckedRunnable runnable, Retry retryContext){
55+
return () -> {
56+
do try {
57+
runnable.run();
58+
} catch (Exception exception) {
59+
retryContext.handleException(exception);
60+
} while (retryContext.isRetryAllowedAfterException());
61+
};
62+
}
63+
64+
static <T, R> Try.CheckedFunction<T, R> retryableCheckedFunction(Try.CheckedFunction<T, R> function, Retry retryContext){
65+
return (T t) -> {
66+
do try {
67+
return function.apply(t);
68+
} catch (Exception exception) {
69+
retryContext.handleException(exception);
70+
} while (retryContext.isRetryAllowedAfterException());
71+
// Should never reach this code
72+
return null;
73+
};
74+
}
75+
76+
static <T> Supplier<T> retryableSupplier(Supplier<T> supplier, Retry retryContext){
77+
return () -> {
78+
do try {
79+
return supplier.get();
80+
} catch (RuntimeException runtimeException) {
81+
retryContext.handleRuntimeException(runtimeException);
82+
} while (retryContext.isRetryAllowedAfterRuntimeException());
83+
// Should never reach this code
84+
return null;
85+
};
86+
}
87+
88+
static <T> Runnable retryableRunnable(Runnable runnable, Retry retryContext){
89+
return () -> {
90+
do try {
91+
runnable.run();
92+
} catch (RuntimeException runtimeException) {
93+
retryContext.handleRuntimeException(runtimeException);
94+
} while (retryContext.isRetryAllowedAfterRuntimeException());
95+
};
96+
}
97+
98+
static <T, R> Function<T, R> retryableFunction(Function<T, R> function, Retry retryContext){
99+
return (T t) -> {
100+
do try {
101+
return function.apply(t);
102+
} catch (RuntimeException runtimeException) {
103+
retryContext.handleRuntimeException(runtimeException);
104+
} while (retryContext.isRetryAllowedAfterRuntimeException());
105+
// Should never reach this code
106+
return null;
107+
};
108+
}
109+
110+
}

0 commit comments

Comments
 (0)