Skip to content

Commit cac5d4e

Browse files
author
Robert Winkler
committed
Issue ReactiveX#13 Store exceptions and allow to retrieve the latest handled exceptions
1 parent b751ff0 commit cac5d4e

8 files changed

+209
-49
lines changed

src/main/java/javaslang/circuitbreaker/CircuitBreakerConfig.java

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,28 @@
2626

2727
public class CircuitBreakerConfig {
2828

29-
private static final int DEFAULT_MAX_FAILURE_THRESHOLD = 50; // Percentage
30-
private static final int DEFAULT_WAIT_DURATION_IN_OPEN_STATE = 60; // Seconds
31-
private static final int DEFAULT_RING_BUFFER_SIZE_IN_HALF_OPEN_STATE = 10;
32-
private static final int DEFAULT_RING_BUFFER_SIZE_IN_CLOSED_STATE = 100;
29+
public static final int DEFAULT_MAX_FAILURE_THRESHOLD = 50; // Percentage
30+
public static final int DEFAULT_WAIT_DURATION_IN_OPEN_STATE = 60; // Seconds
31+
public static final int DEFAULT_RING_BUFFER_SIZE_IN_HALF_OPEN_STATE = 10;
32+
public static final int DEFAULT_RING_BUFFER_SIZE_IN_CLOSED_STATE = 100;
33+
public static final int DEFAULT_EXCEPTION_RING_BUFFER_SIZE = 10;
3334

3435
private final float failureRateThreshold;
35-
private int ringBufferSizeInHalfOpenState;
36-
private int ringBufferSizeInClosedState;
36+
private final int ringBufferSizeInHalfOpenState;
37+
private final int exceptionRingBufferSize;
38+
private final int ringBufferSizeInClosedState;
3739
private final Duration waitDurationInOpenState;
3840
private final CircuitBreakerEventListener circuitBreakerEventListener;
3941
private final Predicate<Throwable> exceptionPredicate;
4042

41-
private CircuitBreakerConfig(float failureRateThreshold,
42-
Duration waitDurationInOpenState,
43-
int ringBufferSizeInHalfOpenState,
44-
int ringBufferSizeInClosedState,
45-
Predicate<Throwable> exceptionPredicate,
46-
CircuitBreakerEventListener circuitBreakerEventListener){
47-
this.failureRateThreshold = failureRateThreshold;
48-
this.waitDurationInOpenState = waitDurationInOpenState;
49-
this.ringBufferSizeInHalfOpenState = ringBufferSizeInHalfOpenState;
50-
this.ringBufferSizeInClosedState = ringBufferSizeInClosedState;
51-
this.exceptionPredicate = exceptionPredicate;
52-
this.circuitBreakerEventListener = circuitBreakerEventListener;
43+
private CircuitBreakerConfig(Context context){
44+
this.failureRateThreshold = context.failureRateThreshold;
45+
this.waitDurationInOpenState = context.waitDurationInOpenState;
46+
this.ringBufferSizeInHalfOpenState = context.ringBufferSizeInHalfOpenState;
47+
this.ringBufferSizeInClosedState = context.ringBufferSizeInClosedState;
48+
this.exceptionRingBufferSize = context.exceptionRingBufferSize;
49+
this.exceptionPredicate = context.exceptionPredicate;
50+
this.circuitBreakerEventListener = context.circuitBreakerEventListener;
5351

5452
}
5553

@@ -77,6 +75,10 @@ public Predicate<Throwable> getExceptionPredicate() {
7775
return exceptionPredicate;
7876
}
7977

78+
public int getExceptionRingBufferSize() {
79+
return exceptionRingBufferSize;
80+
}
81+
8082
/**
8183
* Returns a builder to create a custom CircuitBreakerConfig.
8284
*
@@ -95,15 +97,9 @@ public static CircuitBreakerConfig ofDefaults(){
9597
return new Builder().build();
9698
}
9799

98-
99100
public static class Builder {
100-
private int failureRateThreshold = DEFAULT_MAX_FAILURE_THRESHOLD;
101-
private int ringBufferSizeInHalfOpenState = DEFAULT_RING_BUFFER_SIZE_IN_HALF_OPEN_STATE;
102-
private int ringBufferSizeInClosedState = DEFAULT_RING_BUFFER_SIZE_IN_CLOSED_STATE;
103-
private Duration waitDurationInOpenState = Duration.ofSeconds(DEFAULT_WAIT_DURATION_IN_OPEN_STATE);
104-
private CircuitBreakerEventListener circuitBreakerEventListener = new DefaultCircuitBreakerEventListener();
105-
// The default exception predicate counts all exceptions as failures.
106-
private Predicate<Throwable> exceptionPredicate = (exception) -> true;
101+
102+
private Context context = new Context();
107103

108104
/**
109105
* Configures the failure rate threshold in percentage above which the CircuitBreaker should trip open and start short-circuiting calls.
@@ -117,7 +113,7 @@ public Builder failureRateThreshold(int failureRateThreshold) {
117113
if (failureRateThreshold < 1 || failureRateThreshold > 100) {
118114
throw new IllegalArgumentException("failureRateThreshold must be between 1 and 100");
119115
}
120-
this.failureRateThreshold = failureRateThreshold;
116+
context.failureRateThreshold = failureRateThreshold;
121117
return this;
122118
}
123119

@@ -132,7 +128,7 @@ public Builder waitDurationInOpenState(Duration waitDurationInOpenState) {
132128
if (waitDurationInOpenState.getSeconds() < 1) {
133129
throw new IllegalArgumentException("waitDurationInOpenState must be at least 1000[ms]");
134130
}
135-
this.waitDurationInOpenState = waitDurationInOpenState;
131+
context.waitDurationInOpenState = waitDurationInOpenState;
136132
return this;
137133
}
138134

@@ -150,7 +146,7 @@ public Builder ringBufferSizeInHalfOpenState(int ringBufferSizeInHalfOpenState)
150146
if (ringBufferSizeInHalfOpenState < 1 ) {
151147
throw new IllegalArgumentException("ringBufferSizeInHalfOpenState must be greater than 0");
152148
}
153-
this.ringBufferSizeInHalfOpenState = ringBufferSizeInHalfOpenState;
149+
context.ringBufferSizeInHalfOpenState = ringBufferSizeInHalfOpenState;
154150
return this;
155151
}
156152

@@ -168,7 +164,23 @@ public Builder ringBufferSizeInClosedState(int ringBufferSizeInClosedState) {
168164
if (ringBufferSizeInClosedState < 1) {
169165
throw new IllegalArgumentException("ringBufferSizeInClosedState must be greater than 0");
170166
}
171-
this.ringBufferSizeInClosedState = ringBufferSizeInClosedState;
167+
context.ringBufferSizeInClosedState = ringBufferSizeInClosedState;
168+
return this;
169+
}
170+
171+
/**
172+
* Configures the size of the ring buffer which buffers the latest exceptions which are recorded as a failure and thus increase the failure rate.
173+
*
174+
* Default size is 10. A size of 0 disables buffering.
175+
*
176+
* @param exceptionRingBufferSize the size of the exception ring buffer.
177+
* @return the CircuitBreakerConfig.Builder
178+
*/
179+
public Builder exceptionRingBufferSize(int exceptionRingBufferSize) {
180+
if (exceptionRingBufferSize < 0) {
181+
throw new IllegalArgumentException("exceptionRingBufferSize must be greater than or equal to 0");
182+
}
183+
context.exceptionRingBufferSize = exceptionRingBufferSize;
172184
return this;
173185
}
174186

@@ -182,7 +194,7 @@ public Builder onCircuitBreakerEvent(CircuitBreakerEventListener circuitBreakerE
182194
if (circuitBreakerEventListener == null) {
183195
throw new IllegalArgumentException("circuitBreakerEventListener must not be null");
184196
}
185-
this.circuitBreakerEventListener = circuitBreakerEventListener;
197+
context.circuitBreakerEventListener = circuitBreakerEventListener;
186198
return this;
187199
}
188200

@@ -194,7 +206,7 @@ public Builder onCircuitBreakerEvent(CircuitBreakerEventListener circuitBreakerE
194206
* @return the CircuitBreakerConfig.Builder
195207
*/
196208
public Builder recordFailure(Predicate<Throwable> predicate) {
197-
this.exceptionPredicate = predicate;
209+
context.exceptionPredicate = predicate;
198210
return this;
199211
}
200212

@@ -204,13 +216,18 @@ public Builder recordFailure(Predicate<Throwable> predicate) {
204216
* @return the CircuitBreakerConfig
205217
*/
206218
public CircuitBreakerConfig build() {
207-
return new CircuitBreakerConfig(
208-
failureRateThreshold,
209-
waitDurationInOpenState,
210-
ringBufferSizeInHalfOpenState,
211-
ringBufferSizeInClosedState,
212-
exceptionPredicate,
213-
circuitBreakerEventListener);
219+
return new CircuitBreakerConfig(context);
214220
}
215221
}
222+
223+
private static class Context {
224+
float failureRateThreshold = DEFAULT_MAX_FAILURE_THRESHOLD;
225+
int ringBufferSizeInHalfOpenState = DEFAULT_RING_BUFFER_SIZE_IN_HALF_OPEN_STATE;
226+
int ringBufferSizeInClosedState = DEFAULT_RING_BUFFER_SIZE_IN_CLOSED_STATE;
227+
int exceptionRingBufferSize = DEFAULT_EXCEPTION_RING_BUFFER_SIZE;
228+
Duration waitDurationInOpenState = Duration.ofSeconds(DEFAULT_WAIT_DURATION_IN_OPEN_STATE);
229+
CircuitBreakerEventListener circuitBreakerEventListener = new DefaultCircuitBreakerEventListener();
230+
// The default exception predicate counts all exceptions as failures.
231+
Predicate<Throwable> exceptionPredicate = (exception) -> true;
232+
}
216233
}

src/main/java/javaslang/circuitbreaker/internal/CircuitBreakerMetrics.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,36 @@
2020

2121

2222
import javaslang.circuitbreaker.CircuitBreaker;
23+
import javaslang.circuitbreaker.CircuitBreakerConfig;
24+
import javaslang.collection.List;
2325

2426
class CircuitBreakerMetrics implements CircuitBreaker.Metrics {
2527

2628
private final RingBitSet ringBitSet;
29+
private final CircularFifoBuffer<Throwable> exceptionRingBuffer;
2730

2831
/**
2932
* Maximum number of buffered calls
3033
*/
3134
private int maxNumberOfBufferedCalls;
3235

36+
private int maxNumberOfBufferedExceptions;
37+
3338
CircuitBreakerMetrics(int ringBufferSize) {
3439
this.ringBitSet = new RingBitSet(ringBufferSize);
40+
this.maxNumberOfBufferedExceptions = CircuitBreakerConfig.DEFAULT_EXCEPTION_RING_BUFFER_SIZE;
41+
this.exceptionRingBuffer = new CircularFifoBuffer<>(this.maxNumberOfBufferedExceptions);
42+
this.maxNumberOfBufferedCalls = ringBufferSize;
43+
}
44+
45+
CircuitBreakerMetrics(int ringBufferSize, int exceptionRingBufferSize) {
46+
this.ringBitSet = new RingBitSet(ringBufferSize);
47+
this.maxNumberOfBufferedExceptions = exceptionRingBufferSize;
48+
if(maxNumberOfBufferedExceptions > 0) {
49+
this.exceptionRingBuffer = new CircularFifoBuffer<>(maxNumberOfBufferedExceptions);
50+
}else{
51+
this.exceptionRingBuffer = new CircularFifoBuffer<>(1);
52+
}
3553
this.maxNumberOfBufferedCalls = ringBufferSize;
3654
}
3755

@@ -40,8 +58,11 @@ class CircuitBreakerMetrics implements CircuitBreaker.Metrics {
4058
*
4159
* @return the current failure rate in percentage.
4260
*/
43-
public synchronized float recordFailure(){
61+
public synchronized float recordFailure(Throwable throwable){
4462
ringBitSet.setNextBit(true);
63+
if(maxNumberOfBufferedExceptions > 0) {
64+
exceptionRingBuffer.add(throwable);
65+
}
4566
return getFailureRate();
4667
}
4768

@@ -75,6 +96,15 @@ public long getMaxNumberOfBufferedCalls() {
7596
return maxNumberOfBufferedCalls;
7697
}
7798

99+
/**
100+
* Returns the maximum number of buffered exceptions.
101+
*
102+
* @return the maximum number of buffered exceptions
103+
*/
104+
public long getMaxNumberOfBufferedExceptions() {
105+
return maxNumberOfBufferedExceptions;
106+
}
107+
78108
@Override
79109
public synchronized int getNumberOfBufferedCalls() {
80110
return this.ringBitSet.length();
@@ -84,4 +114,9 @@ public synchronized int getNumberOfBufferedCalls() {
84114
public synchronized int getNumberOfFailedCalls() {
85115
return this.ringBitSet.cardinality();
86116
}
117+
118+
@Override
119+
public List<Throwable> getBufferedExceptions() {
120+
return exceptionRingBuffer.toList();
121+
}
87122
}

src/main/java/javaslang/circuitbreaker/internal/CircuitBreakerState.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ abstract class CircuitBreakerState{
3333

3434
abstract boolean isCallPermitted();
3535

36-
abstract void recordFailure();
36+
abstract void recordFailure(Throwable throwable);
3737

3838
abstract void recordSuccess();
3939

src/main/java/javaslang/circuitbreaker/internal/CircuitBreakerStateMachine.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public boolean isCallPermitted() {
6262
@Override
6363
public void recordFailure(Throwable throwable) {
6464
if(circuitBreakerConfig.getExceptionPredicate().test(throwable)){
65-
stateReference.get().recordFailure();
65+
stateReference.get().recordFailure(throwable);
6666
}
6767
}
6868

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
*
3+
* Copyright 2015 Robert Winkler
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*
18+
*/
19+
package javaslang.circuitbreaker.internal;
20+
21+
import javaslang.collection.List;
22+
23+
import java.util.concurrent.ArrayBlockingQueue;
24+
import java.util.concurrent.locks.ReentrantLock;
25+
26+
/**
27+
* A CircularFifoBuffer is a first in first out buffer with a fixed size that replaces its oldest element if full.
28+
**/
29+
public class CircularFifoBuffer<T> {
30+
31+
private final ArrayBlockingQueue<T> fifoQueue;
32+
private final ReentrantLock lock = new ReentrantLock();
33+
34+
/**
35+
* Creates an {@code CircularFifoBuffer} with the given (fixed)
36+
* capacity
37+
*
38+
* @param capacity the capacity of this CircularFifoBuffer
39+
* @throws IllegalArgumentException if {@code capacity < 1}
40+
*/
41+
public CircularFifoBuffer(int capacity) {
42+
if (capacity < 1) {
43+
throw new IllegalArgumentException("CircularFifoBuffer capacity must be greater than 0");
44+
}
45+
fifoQueue = new ArrayBlockingQueue<>(capacity);
46+
}
47+
48+
/**
49+
* Returns the number of elements in this CircularFifoBuffer.
50+
*
51+
* @return the number of elements in this CircularFifoBuffer
52+
*/
53+
public int size() {
54+
return fifoQueue.size();
55+
}
56+
57+
/**
58+
* Returns <tt>true</tt> if this CircularFifoBuffer contains no elements.
59+
*
60+
* @return <tt>true</tt> if this CircularFifoBuffer contains no elements
61+
*/
62+
public boolean isEmpty() {
63+
return fifoQueue.isEmpty();
64+
}
65+
66+
/**
67+
* Returns <tt>true</tt> if this CircularFifoBuffer is full.
68+
*
69+
* @return <tt>true</tt> if this CircularFifoBuffer is full
70+
*/
71+
public boolean isFull() {
72+
return fifoQueue.remainingCapacity() == 0;
73+
}
74+
75+
/**
76+
* Returns a list containing all of the elements in this CircularFifoBuffer.
77+
* The elements are copied into an array.
78+
*
79+
* @return a list containing all of the elements in this CircularFifoBuffer
80+
*/
81+
public List<T> toList(){
82+
return List.ofAll(fifoQueue);
83+
}
84+
85+
/**
86+
* Overwrites the oldest element when full.
87+
*/
88+
public void add(T element) {
89+
if(!fifoQueue.offer(element)){
90+
final ReentrantLock lock = this.lock;
91+
lock.lock();
92+
try {
93+
fifoQueue.remove();
94+
fifoQueue.add(element);
95+
} finally {
96+
lock.unlock();
97+
}
98+
}
99+
}
100+
}

src/main/java/javaslang/circuitbreaker/internal/ClosedState.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package javaslang.circuitbreaker.internal;
2020

2121
import javaslang.circuitbreaker.CircuitBreaker;
22+
import javaslang.circuitbreaker.CircuitBreakerConfig;
2223

2324
final class ClosedState extends CircuitBreakerState {
2425

@@ -27,7 +28,10 @@ final class ClosedState extends CircuitBreakerState {
2728

2829
ClosedState(CircuitBreakerStateMachine stateMachine) {
2930
super(stateMachine);
30-
this.circuitBreakerMetrics = new CircuitBreakerMetrics(stateMachine.getCircuitBreakerConfig().getRingBufferSizeInClosedState());
31+
CircuitBreakerConfig circuitBreakerConfig = stateMachine.getCircuitBreakerConfig();
32+
this.circuitBreakerMetrics = new CircuitBreakerMetrics(
33+
circuitBreakerConfig.getRingBufferSizeInClosedState(),
34+
circuitBreakerConfig.getExceptionRingBufferSize());
3135
this.failureRateThreshold = stateMachine.getCircuitBreakerConfig().getFailureRateThreshold();
3236
}
3337

@@ -42,8 +46,8 @@ boolean isCallPermitted() {
4246
}
4347

4448
@Override
45-
void recordFailure() {
46-
checkFailureRate(circuitBreakerMetrics.recordFailure());
49+
void recordFailure(Throwable throwable) {
50+
checkFailureRate(circuitBreakerMetrics.recordFailure(throwable));
4751
}
4852

4953
@Override

0 commit comments

Comments
 (0)