Skip to content

Commit

Permalink
Add support for time based circuit breakers
Browse files Browse the repository at this point in the history
  • Loading branch information
jhalterman committed May 15, 2020
1 parent 17dc2f9 commit d2da5a5
Show file tree
Hide file tree
Showing 23 changed files with 1,477 additions and 710 deletions.
27 changes: 26 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
# 2.4.0

### Improvements

- Added time based thresholding support to `CircuitBreaker` via:
- `withFailureThreshold(int failureThreshold, Duration failureThresholdingPeriod)`
- `withFailureThreshold(int failureThreshold, int failureExecutionThreshold, Duration failureThresholdingPeriod)`
- `withFailureRateThreshold(int failureRateThreshold, int failureExecutionThreshold, Duration failureThresholdingPeriod)`
- Added getters to `CircuitBreaker` for existing count based thresholding settings:
- `getFailureThresholdingCapacity()`
- `getSuccessThresholdingCapacity()`
- And added getters to `CircuitBreaker` for new time based thresholding settings:
- `getFailureRateThreshold()`
- `getFailureExecutionThreshold()`
- `getFailureThresholdingPeriod()`
- Added some new metrics to `CircuitBreaker`:
- `getSuccessRate()`
- `getFailureRate()`
- `getExecutionCount()`

### API Changes

- Changed the return type of `CircuitBreaker`'s `getFailureThreshold()` and `getSuccessThreshold()` from `Ratio` to `int`. `getFailureThresholdingCapacity`, `getFailureRateThreshold`, `getFailureExecutionThreshold`, and `getSuccessThresholdingCapacity` provide additional detail about thresholding configuration.
- Removed support for the previously deprecated `CircuitBreaker.withTimeout`. The `Timeout` policy should be used instead.

# 2.3.5

### Bug Fixes
Expand Down Expand Up @@ -53,7 +78,7 @@ Added support for `CompletionStage` to the `Fallback` policy.

# 2.3.0

### API Changes
### Behavior Changes

- `FailsafeExecutor.get` and `FailsafeExecutor.run` will no longer wrap `Error` instances in `FailsafeException` before throwing.

Expand Down
394 changes: 296 additions & 98 deletions src/main/java/net/jodah/failsafe/CircuitBreaker.java

Large diffs are not rendered by default.

10 changes: 0 additions & 10 deletions src/main/java/net/jodah/failsafe/CircuitBreakerExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
*/
package net.jodah.failsafe;

import java.time.Duration;

/**
* A PolicyExecutor that handles failures according to a {@link CircuitBreaker}.
*
Expand All @@ -36,14 +34,6 @@ protected ExecutionResult preExecute() {
return ExecutionResult.failure(new CircuitBreakerOpenException(policy));
}

@Override
protected boolean isFailure(ExecutionResult result) {
long elapsedNanos = execution.getElapsedAttemptTime().toNanos();
Duration timeout = policy.getTimeout();
boolean timeoutExceeded = timeout != null && elapsedNanos >= timeout.toNanos();
return timeoutExceeded || super.isFailure(result);
}

@Override
protected void onSuccess(ExecutionResult result) {
policy.recordSuccess();
Expand Down
41 changes: 18 additions & 23 deletions src/main/java/net/jodah/failsafe/internal/CircuitState.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
*/
package net.jodah.failsafe.internal;

import net.jodah.failsafe.CircuitBreaker;
import net.jodah.failsafe.CircuitBreaker.State;
import net.jodah.failsafe.ExecutionContext;
import net.jodah.failsafe.internal.util.CircularBitSet;
import net.jodah.failsafe.util.Ratio;

import java.time.Duration;

Expand All @@ -28,43 +27,39 @@
* @author Jonathan Halterman
*/
public abstract class CircuitState {
static final Ratio ONE_OF_ONE = new Ratio(1, 1);
final CircuitBreaker breaker;
volatile CircuitStats stats;

protected CircularBitSet bitSet;
CircuitState(CircuitBreaker breaker, CircuitStats stats) {
this.breaker = breaker;
this.stats = stats;
}

public abstract boolean allowsExecution();

public abstract State getInternals();

public Duration getRemainingDelay() {
return Duration.ZERO;
}

public int getFailureCount() {
return bitSet.negatives();
}

public Ratio getFailureRatio() {
return bitSet.negativeRatio();
public CircuitStats getStats() {
return stats;
}

public int getSuccessCount() {
return bitSet.positives();
}

public Ratio getSuccessRatio() {
return bitSet.positiveRatio();
}
public abstract State getState();

public void recordFailure(ExecutionContext context) {
public synchronized void recordFailure(ExecutionContext context) {
stats.recordFailure();
checkThreshold(context);
}

public void recordSuccess() {
public synchronized void recordSuccess() {
stats.recordSuccess();
checkThreshold(null);
}

public void setFailureThreshold(Ratio threshold) {
public void handleConfigChange() {
}

public void setSuccessThreshold(Ratio threshold) {
void checkThreshold(ExecutionContext context) {
}
}
56 changes: 56 additions & 0 deletions src/main/java/net/jodah/failsafe/internal/CircuitStats.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package net.jodah.failsafe.internal;

import net.jodah.failsafe.CircuitBreaker;
import net.jodah.failsafe.internal.TimedCircuitStats.Clock;

/**
* Stats for a circuit breaker.
*/
public interface CircuitStats {
static CircuitStats create(CircuitBreaker breaker, int capacity, boolean supportsTimeBased, CircuitStats oldStats) {
if (supportsTimeBased && breaker.getFailureThresholdingPeriod() != null)
return new TimedCircuitStats(TimedCircuitStats.DEFAULT_BUCKET_COUNT, breaker.getFailureThresholdingPeriod(),
new Clock(), oldStats);
else if (capacity > 1) {
return new CountingCircuitStats(capacity, oldStats);
} else {
return new DefaultCircuitStats();
}
}

default void copyExecutions(CircuitStats oldStats) {
for (int i = 0; i < oldStats.getSuccessCount(); i++)
recordSuccess();
for (int i = 0; i < oldStats.getFailureCount(); i++)
recordSuccess();
}

int getFailureCount();

int getExecutionCount();

int getSuccessCount();

int getFailureRate();

int getSuccessRate();

void recordFailure();

void recordSuccess();
}
57 changes: 21 additions & 36 deletions src/main/java/net/jodah/failsafe/internal/ClosedState.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,13 @@
import net.jodah.failsafe.CircuitBreaker;
import net.jodah.failsafe.CircuitBreaker.State;
import net.jodah.failsafe.ExecutionContext;
import net.jodah.failsafe.internal.util.CircularBitSet;
import net.jodah.failsafe.util.Ratio;

public class ClosedState extends CircuitState {
private final CircuitBreaker breaker;
private final CircuitBreakerInternals internals;

public ClosedState(CircuitBreaker breaker, CircuitBreakerInternals internals) {
this.breaker = breaker;
super(breaker, CircuitStats.create(breaker, capacityFor(breaker), true, null));
this.internals = internals;
setFailureThreshold(breaker.getFailureThreshold() != null ? breaker.getFailureThreshold() : ONE_OF_ONE);
}

@Override
Expand All @@ -37,47 +33,36 @@ public boolean allowsExecution() {
}

@Override
public State getInternals() {
public State getState() {
return State.CLOSED;
}

@Override
public synchronized void recordFailure(ExecutionContext context) {
bitSet.setNext(false);
checkThreshold(context);
}

@Override
public synchronized void recordSuccess() {
bitSet.setNext(true);
checkThreshold(null);
public synchronized void handleConfigChange() {
stats = CircuitStats.create(breaker, capacityFor(breaker), true, stats);
}

/**
* Checks to see if the the executions and failure thresholds have been exceeded, opening the circuit if so.
*/
@Override
public void setFailureThreshold(Ratio threshold) {
bitSet = new CircularBitSet(threshold.getDenominator(), bitSet);
synchronized void checkThreshold(ExecutionContext context) {
// Execution threshold will only be set for time based thresholding
if (stats.getExecutionCount() >= breaker.getFailureExecutionThreshold()) {
double failureRateThreshold = breaker.getFailureRateThreshold();
if ((failureRateThreshold != 0 && stats.getFailureRate() >= failureRateThreshold) || (failureRateThreshold == 0
&& stats.getFailureCount() >= breaker.getFailureThreshold()))
internals.open(context);
}
}

/**
* Checks to determine if a threshold has been met and the circuit should be opened or closed.
*
* <p>
* When a failure ratio is configured, the circuit is opened after the expected number of executions based on whether
* the ratio was exceeded.
* <p>
* If a failure threshold is configured, the circuit is opened if the expected number of executions fails else it's
* closed if a single execution succeeds.
* Returns the capacity of the breaker in the closed state.
*/
synchronized void checkThreshold(ExecutionContext context) {
Ratio failureRatio = breaker.getFailureThreshold();

// Handle failure threshold ratio
if (failureRatio != null && bitSet.occupiedBits() >= failureRatio.getDenominator()
&& bitSet.negativeRatioValue() >= failureRatio.getValue())
internals.open(context);

// Handle no thresholds configured
else if (failureRatio == null && bitSet.negativeRatioValue() == 1)
internals.open(context);
private static int capacityFor(CircuitBreaker<?> breaker) {
if (breaker.getFailureExecutionThreshold() != 0)
return breaker.getFailureExecutionThreshold();
else
return breaker.getFailureThresholdingCapacity();
}
}

0 comments on commit d2da5a5

Please sign in to comment.