diff --git a/src/main/java/net/jodah/failsafe/AsyncFailsafe.java b/src/main/java/net/jodah/failsafe/AsyncFailsafe.java index 59dc4123..024c3600 100644 --- a/src/main/java/net/jodah/failsafe/AsyncFailsafe.java +++ b/src/main/java/net/jodah/failsafe/AsyncFailsafe.java @@ -193,7 +193,6 @@ public AsyncFailsafe with(Listeners listeners) { @SuppressWarnings("unchecked") private FailsafeFuture call(AsyncCallableWrapper callable, FailsafeFuture future) { if (circuitBreaker != null) { - circuitBreaker.initialize(); if (!circuitBreaker.allowsExecution()) throw new CircuitBreakerOpenException(); } diff --git a/src/main/java/net/jodah/failsafe/CircuitBreaker.java b/src/main/java/net/jodah/failsafe/CircuitBreaker.java index 340550ae..7ab426df 100644 --- a/src/main/java/net/jodah/failsafe/CircuitBreaker.java +++ b/src/main/java/net/jodah/failsafe/CircuitBreaker.java @@ -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> failureConditions; @@ -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>(); + state.set(new ClosedState(this)); } /** @@ -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(); } /** @@ -203,8 +181,8 @@ public Integer getSuccessThreshold() { * * @see #withSuccessThreshold(int, int) */ - public Ratio getSuccessThresholdRatio() { - return successThresholdRatio; + public Ratio getSuccessThreshold() { + return successThreshold; } /** @@ -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. */ @@ -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(); } } @@ -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); } /** @@ -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); } /** @@ -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; } @@ -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. */ diff --git a/src/main/java/net/jodah/failsafe/SyncFailsafe.java b/src/main/java/net/jodah/failsafe/SyncFailsafe.java index 647d6881..795f3b87 100644 --- a/src/main/java/net/jodah/failsafe/SyncFailsafe.java +++ b/src/main/java/net/jodah/failsafe/SyncFailsafe.java @@ -143,8 +143,6 @@ public AsyncFailsafe with(Scheduler scheduler) { */ @SuppressWarnings("unchecked") private T call(Callable callable) { - if (circuitBreaker != null) - circuitBreaker.initialize(); Execution execution = new Execution(retryPolicy, circuitBreaker, (ListenerConfig) this); // Handle contextual calls diff --git a/src/main/java/net/jodah/failsafe/internal/CircuitState.java b/src/main/java/net/jodah/failsafe/internal/CircuitState.java index 65e6eece..3675fc5d 100644 --- a/src/main/java/net/jodah/failsafe/internal/CircuitState.java +++ b/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) { + } } \ No newline at end of file diff --git a/src/main/java/net/jodah/failsafe/internal/ClosedState.java b/src/main/java/net/jodah/failsafe/internal/ClosedState.java index edd41b7b..785605db 100644 --- a/src/main/java/net/jodah/failsafe/internal/ClosedState.java +++ b/src/main/java/net/jodah/failsafe/internal/ClosedState.java @@ -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 @@ -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. * @@ -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(); } } \ No newline at end of file diff --git a/src/main/java/net/jodah/failsafe/internal/HalfOpenState.java b/src/main/java/net/jodah/failsafe/internal/HalfOpenState.java index 9e6d1644..6a235ff7 100644 --- a/src/main/java/net/jodah/failsafe/internal/HalfOpenState.java +++ b/src/main/java/net/jodah/failsafe/internal/HalfOpenState.java @@ -5,36 +5,19 @@ import net.jodah.failsafe.internal.util.CircularBitSet; import net.jodah.failsafe.util.Ratio; -public class HalfOpenState implements CircuitState { +public class HalfOpenState extends CircuitState { private final CircuitBreaker circuit; - private final Integer successThresh; - private final Integer failureThresh; - private final Ratio successRatio; - private final Ratio failureRatio; - private final int maxConcurrentExecutions; - - private volatile int executions; - private volatile int successiveSuccesses; - private volatile int successiveFailures; private CircularBitSet bitSet; public HalfOpenState(CircuitBreaker circuit) { this.circuit = circuit; - this.successThresh = circuit.getSuccessThreshold(); - this.failureThresh = circuit.getFailureThreshold(); - this.successRatio = circuit.getSuccessThresholdRatio(); - this.failureRatio = circuit.getFailureThresholdRatio(); - maxConcurrentExecutions = maxConcurrentExecutions(); - - if (successRatio != null) - bitSet = new CircularBitSet(successRatio.denominator); - else if (failureRatio != null) - bitSet = new CircularBitSet(failureRatio.denominator); + setThreshold(circuit.getSuccessThreshold() != null ? circuit.getSuccessThreshold() + : circuit.getFailureThreshold() != null ? circuit.getFailureThreshold() : ONE_OF_ONE); } @Override public boolean allowsExecution(CircuitBreakerStats stats) { - return stats.getCurrentExecutions() < maxConcurrentExecutions; + return stats.getCurrentExecutions() < maxConcurrentExecutions(); } @Override @@ -44,38 +27,19 @@ public State getState() { @Override public synchronized void recordFailure() { - executions++; - successiveFailures++; - successiveSuccesses = 0; - if (bitSet != null) - bitSet.setNext(false); + bitSet.setNext(false); checkThreshold(); } @Override public synchronized void recordSuccess() { - executions++; - successiveSuccesses++; - successiveFailures = 0; - if (bitSet != null) - bitSet.setNext(true); + bitSet.setNext(true); checkThreshold(); } - /** - * Returns the max allowed concurrent executions. - */ - int maxConcurrentExecutions() { - if (successRatio != null) - return successRatio.denominator; - else if (successThresh != null) - return successThresh; - else if (failureRatio != null) - return failureRatio.denominator; - else if (failureThresh != null) - return failureThresh; - else - return 1; + @Override + public void setThreshold(Ratio threshold) { + bitSet = new CircularBitSet(threshold.denominator, bitSet); } /** @@ -85,45 +49,46 @@ else if (failureThresh != null) * If a success ratio is configured, the circuit is opened or closed after the expected number of executions based on * whether the ratio was exceeded. *

- * Else if a success threshold is configured, the circuit is closed if there are an expected number of successive - * successful executions and opened if there is a single failure. - *

* Else if a failure ratio is configured, the circuit is opened or closed after the expected number of executions * based on whether the ratio was not exceeded. *

- * Else if a failure threshold is configured, the circuit is closed if there is a single success else it is opened - * when the expected number of successive failures occur. - *

* Else when no thresholds are configured, the circuit opens or closes on a single failure or success. */ synchronized void checkThreshold() { + Ratio successRatio = circuit.getSuccessThreshold(); + Ratio failureRatio = circuit.getFailureThreshold(); + if (successRatio != null) { - if (executions == successRatio.denominator) + if (bitSet.occupiedBits() == successRatio.denominator + || (successRatio.ratio == 1.0 && bitSet.positiveRatio() < 1.0)) if (bitSet.positiveRatio() >= successRatio.ratio) circuit.close(); else circuit.open(); - } else if (successThresh != null) { - if (successiveSuccesses == successThresh) - circuit.close(); - else if (successiveFailures == 1) - circuit.open(); } else if (failureRatio != null) { - if (executions == failureRatio.denominator) + if (bitSet.occupiedBits() == failureRatio.denominator + || (failureRatio.ratio == 1.0 && bitSet.negativeRatio() < 1.0)) if (bitSet.negativeRatio() >= failureRatio.ratio) circuit.open(); else circuit.close(); - } else if (failureThresh != null) { - if (successiveFailures == failureThresh) - circuit.open(); - else if (successiveSuccesses == 1) - circuit.close(); } else { - if (successiveSuccesses == 1) + if (bitSet.positiveRatio() == 1) circuit.close(); else circuit.open(); } } + + /** + * Returns the max allowed concurrent executions. + */ + int maxConcurrentExecutions() { + if (circuit.getSuccessThreshold() != null) + return circuit.getSuccessThreshold().denominator; + else if (circuit.getFailureThreshold() != null) + return circuit.getFailureThreshold().denominator; + else + return 1; + } } \ No newline at end of file diff --git a/src/main/java/net/jodah/failsafe/internal/OpenState.java b/src/main/java/net/jodah/failsafe/internal/OpenState.java index 0dbae70f..583ccd6c 100644 --- a/src/main/java/net/jodah/failsafe/internal/OpenState.java +++ b/src/main/java/net/jodah/failsafe/internal/OpenState.java @@ -2,21 +2,18 @@ import net.jodah.failsafe.CircuitBreaker; import net.jodah.failsafe.CircuitBreaker.State; -import net.jodah.failsafe.util.Duration; -public class OpenState implements CircuitState { +public class OpenState extends CircuitState { private final CircuitBreaker circuit; - private final Duration delay; private final long startTime = System.nanoTime(); public OpenState(CircuitBreaker circuit) { this.circuit = circuit; - this.delay = circuit.getDelay(); } @Override public boolean allowsExecution(CircuitBreakerStats stats) { - if (System.nanoTime() - startTime >= delay.toNanos()) { + if (System.nanoTime() - startTime >= circuit.getDelay().toNanos()) { circuit.halfOpen(); return true; } @@ -28,12 +25,4 @@ public boolean allowsExecution(CircuitBreakerStats stats) { public State getState() { return State.OPEN; } - - @Override - public void recordFailure() { - } - - @Override - public void recordSuccess() { - } } \ No newline at end of file diff --git a/src/main/java/net/jodah/failsafe/internal/util/CircularBitSet.java b/src/main/java/net/jodah/failsafe/internal/util/CircularBitSet.java index cd78fa39..092de3d7 100644 --- a/src/main/java/net/jodah/failsafe/internal/util/CircularBitSet.java +++ b/src/main/java/net/jodah/failsafe/internal/util/CircularBitSet.java @@ -16,23 +16,36 @@ public class CircularBitSet { private volatile int positives; private volatile int negatives; - public CircularBitSet(int size) { + public CircularBitSet(int size, CircularBitSet oldBitSet) { this.bitSet = new BitSet(size); this.size = size; + + // Initialize from oldBitSet + if (oldBitSet != null) { + for (int i = 0; i < size && i < oldBitSet.occupiedBits; i++) + setNext(oldBitSet.bitSet.get(i)); + } } /** * Returns the ratio of positive bits to the number of occupied bits. */ - public double positiveRatio() { - return (double) positives / (double) occupiedBits; + public double negativeRatio() { + return (double) negatives / (double) occupiedBits; + } + + /** + * Returns the number of occupied bits in the set. + */ + public int occupiedBits() { + return occupiedBits; } /** * Returns the ratio of positive bits to the number of occupied bits. */ - public double negativeRatio() { - return (double) negatives / (double) occupiedBits; + public double positiveRatio() { + return (double) positives / (double) occupiedBits; } /** @@ -66,4 +79,9 @@ public synchronized int setNext(boolean value) { return previousValue; } + + @Override + public String toString() { + return bitSet.toString(); + } } diff --git a/src/test/java/net/jodah/failsafe/CircuitBreakerTest.java b/src/test/java/net/jodah/failsafe/CircuitBreakerTest.java index d7ff442e..8fefa819 100644 --- a/src/test/java/net/jodah/failsafe/CircuitBreakerTest.java +++ b/src/test/java/net/jodah/failsafe/CircuitBreakerTest.java @@ -70,29 +70,21 @@ public void shouldRequireValidTimeout() { public void shouldRequireValidFailureThreshold() { assertThrows(() -> new CircuitBreaker().withFailureThreshold(0), IllegalArgumentException.class); - assertThrows(() -> new CircuitBreaker().withFailureThreshold(2, 2).withFailureThreshold(2), - IllegalStateException.class); } public void shouldRequireValidFailureThresholdRatio() { assertThrows(() -> new CircuitBreaker().withFailureThreshold(0, 2), IllegalArgumentException.class); assertThrows(() -> new CircuitBreaker().withFailureThreshold(2, 0), IllegalArgumentException.class); assertThrows(() -> new CircuitBreaker().withFailureThreshold(2, 1), IllegalArgumentException.class); - assertThrows(() -> new CircuitBreaker().withFailureThreshold(2).withFailureThreshold(2, 2), - IllegalStateException.class); } public void shouldRequireValidSuccessThreshold() { assertThrows(() -> new CircuitBreaker().withSuccessThreshold(0), IllegalArgumentException.class); - assertThrows(() -> new CircuitBreaker().withSuccessThreshold(2, 2).withSuccessThreshold(2), - IllegalStateException.class); } public void shouldRequireValidSuccessThresholdRatio() { assertThrows(() -> new CircuitBreaker().withSuccessThreshold(0, 2), IllegalArgumentException.class); assertThrows(() -> new CircuitBreaker().withSuccessThreshold(2, 0), IllegalArgumentException.class); assertThrows(() -> new CircuitBreaker().withSuccessThreshold(2, 1), IllegalArgumentException.class); - assertThrows(() -> new CircuitBreaker().withSuccessThreshold(2).withSuccessThreshold(2, 2), - IllegalStateException.class); } } diff --git a/src/test/java/net/jodah/failsafe/FailsafeTest.java b/src/test/java/net/jodah/failsafe/FailsafeTest.java index c32a5eaa..a4835fac 100644 --- a/src/test/java/net/jodah/failsafe/FailsafeTest.java +++ b/src/test/java/net/jodah/failsafe/FailsafeTest.java @@ -2,17 +2,11 @@ import static org.testng.Assert.assertTrue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import org.mockito.Mockito; import org.testng.annotations.Test; -import net.jodah.failsafe.AsyncFailsafe; -import net.jodah.failsafe.Failsafe; -import net.jodah.failsafe.RetryPolicy; -import net.jodah.failsafe.SyncFailsafe; import net.jodah.failsafe.util.concurrent.Scheduler; @Test @@ -24,27 +18,5 @@ public void testWithExecutor() { assertTrue(Failsafe.with(new RetryPolicy()) instanceof SyncFailsafe); assertTrue(Failsafe.with(new RetryPolicy()).with(executor) instanceof AsyncFailsafe); assertTrue(Failsafe.with(new RetryPolicy()).with(scheduler) instanceof AsyncFailsafe); - - } - - public void test() throws Throwable { - CompletableFuture.completedFuture("test").whenComplete((r, f) -> test(r)); - - Failsafe.with(new RetryPolicy()) - .with(Executors.newScheduledThreadPool(1)) - .onFailedAttempt((String r, Throwable e) -> test(r)) - .onFailedAttemptAsync((String r, Throwable e) -> test(r)) - .onComplete((String r, Throwable t) -> test(r)) - .run(() -> {}); - - Failsafe.with(new RetryPolicy()) - .with(Executors.newScheduledThreadPool(1)) - .onFailedAttempt((r, e) -> test(r)) - .onFailedAttemptAsync((r, e) -> test(r)) - .onComplete((r, t) -> test(r)) - .get(() -> "asdf"); - } - - private void test(String adsf) { } } diff --git a/src/test/java/net/jodah/failsafe/Testing.java b/src/test/java/net/jodah/failsafe/Testing.java index b8685e0c..3564e392 100644 --- a/src/test/java/net/jodah/failsafe/Testing.java +++ b/src/test/java/net/jodah/failsafe/Testing.java @@ -1,8 +1,11 @@ package net.jodah.failsafe; +import java.lang.reflect.Field; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; import net.jodah.failsafe.function.CheckedRunnable; +import net.jodah.failsafe.internal.CircuitState; public class Testing { public static Throwable getThrowable(CheckedRunnable runnable) { @@ -43,4 +46,16 @@ public static void runInThread(CheckedRunnable runnable) { public static void noop() { } + + @SuppressWarnings("unchecked") + public static T stateFor(CircuitBreaker breaker) { + Field stateField; + try { + stateField = CircuitBreaker.class.getDeclaredField("state"); + stateField.setAccessible(true); + return (T) ((AtomicReference) stateField.get(breaker)).get(); + } catch (Exception e) { + return null; + } + } } diff --git a/src/test/java/net/jodah/failsafe/internal/ClosedStateTest.java b/src/test/java/net/jodah/failsafe/internal/ClosedStateTest.java index 61923b83..dd08999d 100644 --- a/src/test/java/net/jodah/failsafe/internal/ClosedStateTest.java +++ b/src/test/java/net/jodah/failsafe/internal/ClosedStateTest.java @@ -6,23 +6,13 @@ import org.testng.annotations.Test; import net.jodah.failsafe.CircuitBreaker; +import net.jodah.failsafe.Testing; @Test public class ClosedStateTest { - public void testSuccessWithDefaultConfig() { - // Given - CircuitBreaker breaker = new CircuitBreaker(); - breaker.close(); - ClosedState state = new ClosedState(breaker); - assertTrue(breaker.isClosed()); - - // When - state.recordSuccess(); - - // Then - assertTrue(breaker.isClosed()); - } - + /** + * Asserts that the the circuit is opened after a single failure. + */ public void testFailureWithDefaultConfig() { // Given CircuitBreaker breaker = new CircuitBreaker(); @@ -37,22 +27,28 @@ public void testFailureWithDefaultConfig() { assertTrue(breaker.isOpen()); } - public void testSuccessWithFailureThreshold() { + /** + * Asserts that the the circuit is opened after the failure ratio is met. + */ + public void testFailureWithFailureRatio() { // Given - CircuitBreaker breaker = new CircuitBreaker().withFailureThreshold(3); + CircuitBreaker breaker = new CircuitBreaker().withFailureThreshold(2, 3); breaker.close(); ClosedState state = new ClosedState(breaker); - assertTrue(breaker.isClosed()); // When + state.recordFailure(); state.recordSuccess(); - state.recordSuccess(); - state.recordSuccess(); + assertTrue(breaker.isClosed()); + state.recordFailure(); // Then - assertTrue(breaker.isClosed()); + assertTrue(breaker.isOpen()); } + /** + * Asserts that the the circuit is opened after the failure threshold is met. + */ public void testFailureWithFailureThreshold() { // Given CircuitBreaker breaker = new CircuitBreaker().withFailureThreshold(3); @@ -72,10 +68,26 @@ public void testFailureWithFailureThreshold() { } /** - * Asserts that after numerous execution outcomes, when the last 3 out of 4 executions eventually are failures, the - * circuit opens. + * Asserts that the the circuit is still closed after a single success. + */ + public void testSuccessWithDefaultConfig() { + // Given + CircuitBreaker breaker = new CircuitBreaker(); + breaker.close(); + ClosedState state = new ClosedState(breaker); + assertTrue(breaker.isClosed()); + + // When + state.recordSuccess(); + + // Then + assertTrue(breaker.isClosed()); + } + + /** + * Asserts that the the circuit stays closed after the failure ratio fails to be met. */ - public void testSuccessWithFailureThresholdRatio() { + public void testSuccessWithFailureRatio() { // Given CircuitBreaker breaker = new CircuitBreaker().withFailureThreshold(3, 4); breaker.close(); @@ -85,27 +97,59 @@ public void testSuccessWithFailureThresholdRatio() { // When / Then for (int i = 0; i < 20; i++) { state.recordSuccess(); + state.recordFailure(); assertTrue(breaker.isClosed()); } } - public void testFailureWithFailureThresholdRatio() { + /** + * Asserts that the the circuit stays closed after the failure ratio fails to be met. + */ + public void testSuccessWithFailureThreshold() { // Given - CircuitBreaker breaker = new CircuitBreaker().withFailureThreshold(3, 4); + CircuitBreaker breaker = new CircuitBreaker().withFailureThreshold(2); breaker.close(); ClosedState state = new ClosedState(breaker); + assertTrue(breaker.isClosed()); - // When - for (int i = 0; i < 10; i++) { + // When / Then + for (int i = 0; i < 20; i++) { state.recordSuccess(); state.recordFailure(); assertTrue(breaker.isClosed()); - state.recordFailure(); - assertTrue(breaker.isClosed()); - state.recordSuccess(); - assertTrue(breaker.isClosed()); } + } + /** + * Asserts that the late configuration of a failure ratio is handled by resetting the state's internal tracking. Also + * asserts that executions from prior configurations are carried over to a new configuration. + */ + public void shouldHandleLateSetFailureRatio() { + // Given + CircuitBreaker breaker = new CircuitBreaker(); + ClosedState state = Testing.stateFor(breaker); + + // When + state.recordSuccess(); + assertTrue(breaker.isClosed()); + breaker.withFailureThreshold(2); + state.recordFailure(); + assertTrue(breaker.isClosed()); + state.recordFailure(); + + // Then + assertTrue(breaker.isOpen()); + + // Given + breaker = new CircuitBreaker(); + state = Testing.stateFor(breaker); + + // When + state.recordSuccess(); + assertTrue(breaker.isClosed()); + breaker.withFailureThreshold(2, 3); + state.recordFailure(); + assertTrue(breaker.isClosed()); state.recordFailure(); // Then diff --git a/src/test/java/net/jodah/failsafe/internal/HalfOpenStateTest.java b/src/test/java/net/jodah/failsafe/internal/HalfOpenStateTest.java index 524c1719..b0147497 100644 --- a/src/test/java/net/jodah/failsafe/internal/HalfOpenStateTest.java +++ b/src/test/java/net/jodah/failsafe/internal/HalfOpenStateTest.java @@ -6,6 +6,7 @@ import org.testng.annotations.Test; import net.jodah.failsafe.CircuitBreaker; +import net.jodah.failsafe.Testing; import net.jodah.failsafe.internal.HalfOpenState; @Test @@ -164,6 +165,7 @@ public void testFailureWithSuccessThreshold() { assertFalse(breaker.isClosed()); // When + state.recordSuccess(); state.recordFailure(); // Then @@ -238,6 +240,7 @@ public void testSuccessWithFailureThreshold() { assertFalse(breaker.isClosed()); // When + state.recordFailure(); state.recordSuccess(); // Then @@ -364,4 +367,77 @@ public void testSuccessWithSuccessThresholdAndFailureRatio() { // Then assertTrue(breaker.isClosed()); } + + /** + * Asserts that the late configuration of a failure ratio is handled by resetting the state's internal tracking. Also + * asserts that executions from prior configurations are carried over to a new configuration. + */ + public void shouldHandleLateSetFailureRatio() { + // Given + CircuitBreaker breaker = new CircuitBreaker(); + breaker.halfOpen(); + HalfOpenState state = Testing.stateFor(breaker); + + // When + breaker.withFailureThreshold(2); + state.recordFailure(); + assertTrue(breaker.isHalfOpen()); + state.recordFailure(); + + // Then + assertTrue(breaker.isOpen()); + + // Given + breaker = new CircuitBreaker().withFailureThreshold(3); + breaker.halfOpen(); + state = Testing.stateFor(breaker); + + // When + state.recordFailure(); + state.recordFailure(); + breaker.withFailureThreshold(3, 5); + state.recordSuccess(); + state.recordSuccess(); + assertTrue(breaker.isHalfOpen()); + state.recordFailure(); + + // Then + assertTrue(breaker.isOpen()); + } + + /** + * Asserts that the late configuration of a success ratio is handled by resetting the state's internal tracking. Also + * asserts that executions from prior configurations are carried over to a new configuration. + */ + public void shouldHandleLateSetSucessRatio() { + // Given + CircuitBreaker breaker = new CircuitBreaker(); + breaker.halfOpen(); + HalfOpenState state = Testing.stateFor(breaker); + + // When + breaker.withSuccessThreshold(2); + state.recordSuccess(); + assertTrue(breaker.isHalfOpen()); + state.recordSuccess(); + + // Then + assertTrue(breaker.isClosed()); + + // Given + breaker = new CircuitBreaker().withFailureThreshold(3); + breaker.halfOpen(); + state = Testing.stateFor(breaker); + + // When + state.recordFailure(); + state.recordFailure(); + breaker.withSuccessThreshold(2, 4); + state.recordSuccess(); + assertTrue(breaker.isHalfOpen()); + state.recordSuccess(); + + // Then + assertTrue(breaker.isClosed()); + } } diff --git a/src/test/java/net/jodah/failsafe/internal/util/CircularBitSetTest.java b/src/test/java/net/jodah/failsafe/internal/util/CircularBitSetTest.java index 89c2c870..076e2c80 100644 --- a/src/test/java/net/jodah/failsafe/internal/util/CircularBitSetTest.java +++ b/src/test/java/net/jodah/failsafe/internal/util/CircularBitSetTest.java @@ -9,7 +9,7 @@ @Test public class CircularBitSetTest { public void shouldReturnUnitializedValues() { - CircularBitSet bs = new CircularBitSet(100); + CircularBitSet bs = new CircularBitSet(100, null); for (int i = 0; i < 100; i++) { assertEquals(bs.setNext(true), -1); } @@ -19,7 +19,7 @@ public void shouldReturnUnitializedValues() { } public void testRatios() { - CircularBitSet bs = new CircularBitSet(100); + CircularBitSet bs = new CircularBitSet(100, null); for (int i = 0; i < 50; i++) bs.setNext(i % 3 == 0); @@ -31,4 +31,20 @@ public void testRatios() { assertEquals(bs.positiveRatio(), 1.0); assertEquals(bs.negativeRatio(), 0.0); } + + public void shouldCopyBits() { + CircularBitSet bs = new CircularBitSet(50, null); + for (int i = 0; i < 50; i++) + bs.setNext(i % 2 == 0); + + CircularBitSet bs2 = new CircularBitSet(26, bs); + assertEquals(bs2.occupiedBits(), 26); + assertEquals(bs2.positiveRatio(), .5); + assertEquals(bs2.negativeRatio(), .5); + + CircularBitSet bs3 = new CircularBitSet(100, bs); + assertEquals(bs3.occupiedBits(), 50); + assertEquals(bs3.positiveRatio(), .5); + assertEquals(bs3.negativeRatio(), .5); + } }