Skip to content

Commit

Permalink
CircuitBreakers default to closed
Browse files Browse the repository at this point in the history
* Circuit breakers default to closed
* Circuit breakers can be re-configured at any time, and executions recorded from previous configuration will be carried over.
* Simplified internal circuit state tracking
* Fixes #34
  • Loading branch information
jhalterman committed Jul 16, 2016
1 parent 0209e97 commit efcd0a0
Show file tree
Hide file tree
Showing 14 changed files with 307 additions and 270 deletions.
1 change: 0 additions & 1 deletion src/main/java/net/jodah/failsafe/AsyncFailsafe.java
Expand Up @@ -193,7 +193,6 @@ public <T> AsyncFailsafe<T> with(Listeners<T> listeners) {
@SuppressWarnings("unchecked")
private <T> FailsafeFuture<T> call(AsyncCallableWrapper<T> callable, FailsafeFuture<T> future) {
if (circuitBreaker != null) {
circuitBreaker.initialize();
if (!circuitBreaker.allowsExecution())
throw new CircuitBreakerOpenException();
}
Expand Down
129 changes: 42 additions & 87 deletions src/main/java/net/jodah/failsafe/CircuitBreaker.java
Expand Up @@ -36,10 +36,8 @@ public int getCurrentExecutions() {
};
private Duration delay = Duration.NONE;
private Duration timeout;
private Integer failureThreshold;
private Ratio failureThresholdRatio;
private Integer successThreshold;
private Ratio successThresholdRatio;
private Ratio failureThreshold;
private Ratio successThreshold;
/** Indicates whether failures are checked by a configured failure condition */
private boolean failuresChecked;
private List<BiPredicate<Object, Throwable>> failureConditions;
Expand All @@ -48,10 +46,11 @@ public int getCurrentExecutions() {
CheckedRunnable onClose;

/**
* Creates a Circuit that opens after a single failure, closes after a single success, and has no delay.
* Creates a Circuit that opens after a single failure, closes after a single success, and has no delay by default.
*/
public CircuitBreaker() {
failureConditions = new ArrayList<BiPredicate<Object, Throwable>>();
state.set(new ClosedState(this));
}

/**
Expand Down Expand Up @@ -160,41 +159,20 @@ public Duration getDelay() {
return delay;
}

/**
* Returns the number of successive failures that must occur when in a closed state in order to open the circuit else
* null if none has been configured.
*
* @see #withFailureThreshold(int)
*/
public Integer getFailureThreshold() {
return failureThreshold;
}

/**
* Gets the ratio of successive failures that must occur when in a closed state in order to open the circuit.
*
* @see #withFailureThreshold(int, int)
*/
public Ratio getFailureThresholdRatio() {
return failureThresholdRatio;
public Ratio getFailureThreshold() {
return failureThreshold;
}

/**
* Gets the state of the circuit.
*/
public State getState() {
CircuitState circuitState = state.get();
return circuitState == null ? State.CLOSED : circuitState.getState();
}

/**
* Returns the number of successive successful executions that must occur when in a half-open state in order to close
* the circuit else null if none has been configured.
*
* @see #withSuccessThreshold(int)
*/
public Integer getSuccessThreshold() {
return successThreshold;
return state.get().getState();
}

/**
Expand All @@ -203,8 +181,8 @@ public Integer getSuccessThreshold() {
*
* @see #withSuccessThreshold(int, int)
*/
public Ratio getSuccessThresholdRatio() {
return successThresholdRatio;
public Ratio getSuccessThreshold() {
return successThreshold;
}

/**
Expand Down Expand Up @@ -251,6 +229,13 @@ public boolean isFailure(Object result, Throwable failure) {
return failure != null && !failuresChecked;
}

/**
* Returns whether the circuit is half open.
*/
public boolean isHalfOpen() {
return State.HALF_OPEN.equals(getState());
}

/**
* Returns whether the circuit is open.
*/
Expand Down Expand Up @@ -310,13 +295,10 @@ public void recordResult(Object result) {
* Records an execution success.
*/
public void recordSuccess() {
CircuitState circuitState = state.get();
if (state != null) {
try {
circuitState.recordSuccess();
} finally {
currentExecutions.decrementAndGet();
}
try {
state.get().recordSuccess();
} finally {
currentExecutions.decrementAndGet();
}
}

Expand All @@ -339,19 +321,13 @@ public CircuitBreaker withDelay(long delay, TimeUnit timeUnit) {
}

/**
* Sets the number of successive failures that must occur when in a closed state in order to open the circuit. 0
* represents no threshold.
* Sets the number of successive failures that must occur when in a closed state in order to open the circuit.
*
* @throws IllegalArgumentException if {@code failureThresh} < 1
* @throws IllegalStateException if a failure ratio has already been configured via
* {@link #withFailureThreshold(int, int)}
*/
public CircuitBreaker withFailureThreshold(int failureThreshold) {
Assert.isTrue(failureThreshold >= 1, "failureThreshold must be greater than or equal to 1");
Assert.state(failureThresholdRatio == null,
"failure threshold and failure threshold ratio cannot both be configured");
this.failureThreshold = failureThreshold;
return this;
return withFailureThreshold(failureThreshold, failureThreshold);
}

/**
Expand All @@ -363,31 +339,26 @@ public CircuitBreaker withFailureThreshold(int failureThreshold) {
* @param executions The number of executions to measure the {@code failures} against
* @throws IllegalArgumentException if {@code failures} < 1, {@code executions} < 1, or {@code failures} is <
* {@code executions}
* @throws IllegalStateException if a failure ratio has already been configured via {@link #withFailureThreshold(int)}
*/
public CircuitBreaker withFailureThreshold(int failures, int executions) {
Assert.isTrue(failures >= 1, "failures must be greater than or equal to 1");
Assert.isTrue(executions >= 1, "executions must be greater than or equal to 1");
Assert.isTrue(executions >= failures, "executions must be greater than or equal to failures");
Assert.state(failureThreshold == null, "failure threshold and failure threshold ratio cannot both be configured");
this.failureThresholdRatio = new Ratio(failures, executions);
this.failureThreshold = new Ratio(failures, executions);
if (successThreshold == null)
state.get().setThreshold(failureThreshold);
return this;
}

/**
* Sets the number of successive successful executions that must occur when in a half-open state in order to close the
* circuit. 0 represents no threshold.
* circuit, else the circuit is re-opened when a failure occurs.
*
* @throws IllegalArgumentException if {@code successThreshold} < 1
* @throws IllegalStateException if a success ratio has already been configured via
* {@link #withSuccessThreshold(int, int)}
*/
public CircuitBreaker withSuccessThreshold(int successThreshold) {
Assert.isTrue(successThreshold >= 1, "successThreshold must be greater than or equal to 1");
Assert.state(successThresholdRatio == null,
"success threshold and success threshold ratio cannot both be configured");
this.successThreshold = successThreshold;
return this;
return withSuccessThreshold(successThreshold, successThreshold);
}

/**
Expand All @@ -399,15 +370,13 @@ public CircuitBreaker withSuccessThreshold(int successThreshold) {
* @param executions The number of executions to measure the {@code successes} against
* @throws IllegalArgumentException if {@code successes} < 1, {@code executions} < 1, or {@code successes} is <
* {@code executions}
* @throws IllegalStateException if a success threshold has already been configured via
* {@link #withSuccessThreshold(int)}
*/
public CircuitBreaker withSuccessThreshold(int successes, int executions) {
Assert.isTrue(successes >= 1, "successes must be greater than or equal to 1");
Assert.isTrue(executions >= 1, "executions must be greater than or equal to 1");
Assert.isTrue(executions >= successes, "executions must be greater than or equal to successes");
Assert.state(successThreshold == null, "success threshold and success threshold ratio cannot both be configured");
this.successThresholdRatio = new Ratio(successes, executions);
this.successThreshold = new Ratio(successes, executions);
state.get().setThreshold(successThreshold);
return this;
}

Expand All @@ -424,46 +393,32 @@ public CircuitBreaker withTimeout(long timeout, TimeUnit timeUnit) {
return this;
}

/**
* Initializes the circuit prior to use.
*/
synchronized void initialize() {
if (state.get() == null)
state.set(new ClosedState(this));
void before() {
currentExecutions.incrementAndGet();
}

/**
* Records an execution failure.
*/
void recordFailure() {
CircuitState circuitState = state.get();
if (state != null) {
try {
circuitState.recordFailure();
} finally {
currentExecutions.decrementAndGet();
}
try {
state.get().recordFailure();
} finally {
currentExecutions.decrementAndGet();
}
}

void recordResult(Object result, Throwable failure) {
CircuitState circuitState = state.get();
if (state != null) {
try {
if (isFailure(result, failure))
circuitState.recordFailure();
else
circuitState.recordSuccess();
} finally {
currentExecutions.decrementAndGet();
}
try {
if (isFailure(result, failure))
state.get().recordFailure();
else
state.get().recordSuccess();
} finally {
currentExecutions.decrementAndGet();
}
}

void before() {
currentExecutions.incrementAndGet();
}

/**
* Transitions to the {@code newState} if not already in that state and calls any associated event listener.
*/
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/net/jodah/failsafe/SyncFailsafe.java
Expand Up @@ -143,8 +143,6 @@ public AsyncFailsafe<R> with(Scheduler scheduler) {
*/
@SuppressWarnings("unchecked")
private <T> T call(Callable<T> callable) {
if (circuitBreaker != null)
circuitBreaker.initialize();
Execution execution = new Execution(retryPolicy, circuitBreaker, (ListenerConfig<?, Object>) this);

// Handle contextual calls
Expand Down
18 changes: 13 additions & 5 deletions src/main/java/net/jodah/failsafe/internal/CircuitState.java
@@ -1,18 +1,26 @@
package net.jodah.failsafe.internal;

import net.jodah.failsafe.CircuitBreaker.State;
import net.jodah.failsafe.util.Ratio;

/**
* The state of a circuit.
*
* @author Jonathan Halterman
*/
public interface CircuitState {
boolean allowsExecution(CircuitBreakerStats stats);
public abstract class CircuitState {
static final Ratio ONE_OF_ONE = new Ratio(1, 1);

State getState();
public abstract boolean allowsExecution(CircuitBreakerStats stats);

void recordFailure();
public abstract State getState();

void recordSuccess();
public void recordFailure() {
}

public void recordSuccess() {
}

public void setThreshold(Ratio threshold) {
}
}
38 changes: 14 additions & 24 deletions src/main/java/net/jodah/failsafe/internal/ClosedState.java
Expand Up @@ -5,21 +5,13 @@
import net.jodah.failsafe.internal.util.CircularBitSet;
import net.jodah.failsafe.util.Ratio;

public class ClosedState implements CircuitState {
public class ClosedState extends CircuitState {
private final CircuitBreaker circuit;
private final Integer failureThresh;
private final Ratio failureRatio;

private volatile int executions;
private volatile int successiveFailures;
private CircularBitSet bitSet;

public ClosedState(CircuitBreaker circuit) {
this.circuit = circuit;
this.failureThresh = circuit.getFailureThreshold();
this.failureRatio = circuit.getFailureThresholdRatio();
if (failureRatio != null)
bitSet = new CircularBitSet(failureRatio.denominator);
setThreshold(circuit.getFailureThreshold() != null ? circuit.getFailureThreshold() : ONE_OF_ONE);
}

@Override
Expand All @@ -34,22 +26,21 @@ public State getState() {

@Override
public synchronized void recordFailure() {
executions++;
successiveFailures++;
if (bitSet != null)
bitSet.setNext(false);
bitSet.setNext(false);
checkThreshold();
}

@Override
public synchronized void recordSuccess() {
executions++;
successiveFailures = 0;
if (bitSet != null)
bitSet.setNext(true);
bitSet.setNext(true);
checkThreshold();
}

@Override
public void setThreshold(Ratio threshold) {
bitSet = new CircularBitSet(threshold.denominator, bitSet);
}

/**
* Checks to determine if a threshold has been met and the circuit should be opened or closed.
*
Expand All @@ -61,16 +52,15 @@ public synchronized void recordSuccess() {
* closed if a single execution succeeds.
*/
synchronized void checkThreshold() {
// Handle failure threshold ratio
if (failureRatio != null && executions >= failureRatio.denominator && bitSet.negativeRatio() >= failureRatio.ratio)
circuit.open();
Ratio failureRatio = circuit.getFailureThreshold();

// Handle failure threshold
if (failureThresh != null && successiveFailures == failureThresh)
// Handle failure threshold ratio
if (failureRatio != null && bitSet.occupiedBits() >= failureRatio.denominator
&& bitSet.negativeRatio() >= failureRatio.ratio)
circuit.open();

// Handle no thresholds configured
if (failureThresh == null && failureRatio == null && successiveFailures == 1)
if (failureRatio == null && bitSet.negativeRatio() == 1)
circuit.open();
}
}

0 comments on commit efcd0a0

Please sign in to comment.