diff --git a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreakerConfig.java b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreakerConfig.java index 5d281a3743..fde1b733bd 100644 --- a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreakerConfig.java +++ b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/CircuitBreakerConfig.java @@ -41,6 +41,7 @@ public class CircuitBreakerConfig { private Duration waitDurationInOpenState = Duration.ofSeconds(DEFAULT_WAIT_DURATION_IN_OPEN_STATE); // The default exception predicate counts all exceptions as failures. private Predicate recordFailurePredicate = DEFAULT_RECORD_FAILURE_PREDICATE; + private boolean automaticTransitionFromOpenToHalfOpenEnabled = false; private CircuitBreakerConfig(){ } @@ -64,7 +65,11 @@ public int getRingBufferSizeInClosedState() { public Predicate getRecordFailurePredicate() { return recordFailurePredicate; } - + + public boolean isAutomaticTransitionFromOpenToHalfOpenEnabled() { + return automaticTransitionFromOpenToHalfOpenEnabled; + } + /** * Returns a builder to create a custom CircuitBreakerConfig. * @@ -94,6 +99,7 @@ public static class Builder { private int ringBufferSizeInHalfOpenState = DEFAULT_RING_BUFFER_SIZE_IN_HALF_OPEN_STATE; private int ringBufferSizeInClosedState = DEFAULT_RING_BUFFER_SIZE_IN_CLOSED_STATE; private Duration waitDurationInOpenState = Duration.ofSeconds(DEFAULT_WAIT_DURATION_IN_OPEN_STATE); + private boolean automaticTransitionFromOpenToHalfOpenEnabled = false; /** * Configures the failure rate threshold in percentage above which the CircuitBreaker should trip open and start short-circuiting calls. @@ -220,6 +226,16 @@ public final Builder ignoreExceptions(Class... errorClasses return this; } + /** + * Enables automatic transition from OPEN to HALF_OPEN state once the waitDurationInOpenState has passed. + * + * @return the CircuitBreakerConfig.Builder + */ + public Builder enableAutomaticTransitionFromOpenToHalfOpen() { + this.automaticTransitionFromOpenToHalfOpenEnabled = true; + return this; + } + /** * Builds a CircuitBreakerConfig * @@ -233,6 +249,7 @@ public CircuitBreakerConfig build() { config.ringBufferSizeInClosedState = ringBufferSizeInClosedState; config.ringBufferSizeInHalfOpenState = ringBufferSizeInHalfOpenState; config.recordFailurePredicate = errorRecordingPredicate; + config.automaticTransitionFromOpenToHalfOpenEnabled = automaticTransitionFromOpenToHalfOpenEnabled; return config; } diff --git a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/AutoTransitioner.java b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/AutoTransitioner.java new file mode 100644 index 0000000000..eb0ff9bd4b --- /dev/null +++ b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/AutoTransitioner.java @@ -0,0 +1,28 @@ +package io.github.resilience4j.circuitbreaker.internal; + +import io.vavr.Lazy; + +import java.time.Duration; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Schedules tasks to be completed after a duration. E.g. to automatically transition from open to half open state + * when automaticTransitionFromOpenToHalfOpenEnabled config property is set to true. + */ +public class AutoTransitioner { + + private static final Lazy executorService = Lazy.of( + Executors::newSingleThreadScheduledExecutor); + + private AutoTransitioner() { + } + + public static void scheduleAutoTransition(Runnable transition, Duration waitDurationInOpenState) { + executorService.get().schedule( + transition, + waitDurationInOpenState.toMillis(), + TimeUnit.MILLISECONDS); + } +} \ No newline at end of file diff --git a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/OpenState.java b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/OpenState.java index 5fe98df4ae..a237c993a8 100644 --- a/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/OpenState.java +++ b/resilience4j-circuitbreaker/src/main/java/io/github/resilience4j/circuitbreaker/internal/OpenState.java @@ -20,6 +20,7 @@ import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import java.time.Duration; import java.time.Instant; final class OpenState extends CircuitBreakerState { @@ -29,8 +30,13 @@ final class OpenState extends CircuitBreakerState { OpenState(CircuitBreakerStateMachine stateMachine, CircuitBreakerMetrics circuitBreakerMetrics) { super(stateMachine); - this.retryAfterWaitDuration = Instant.now().plus(stateMachine.getCircuitBreakerConfig().getWaitDurationInOpenState()); + final Duration waitDurationInOpenState = stateMachine.getCircuitBreakerConfig().getWaitDurationInOpenState(); + this.retryAfterWaitDuration = Instant.now().plus(waitDurationInOpenState); this.circuitBreakerMetrics = circuitBreakerMetrics; + + if (stateMachine.getCircuitBreakerConfig().isAutomaticTransitionFromOpenToHalfOpenEnabled()) { + AutoTransitioner.scheduleAutoTransition(stateMachine::transitionToHalfOpenState, waitDurationInOpenState); + } } /** diff --git a/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerAutoTransitionStateMachineTest.java b/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerAutoTransitionStateMachineTest.java new file mode 100644 index 0000000000..7c186c6f0c --- /dev/null +++ b/resilience4j-circuitbreaker/src/test/java/io/github/resilience4j/circuitbreaker/internal/CircuitBreakerAutoTransitionStateMachineTest.java @@ -0,0 +1,288 @@ +/* + * + * Copyright 2016 Robert Winkler + * + * 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 io.github.resilience4j.circuitbreaker.internal; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import io.github.resilience4j.circuitbreaker.event.CircuitBreakerOnStateTransitionEvent; +import io.github.resilience4j.core.EventConsumer; +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static java.lang.Thread.sleep; +import static org.assertj.core.api.BDDAssertions.assertThat; + +public class CircuitBreakerAutoTransitionStateMachineTest { + + private final List circuitBreakersGroupA = new ArrayList<>(); + private final List circuitBreakersGroupB = new ArrayList<>(); + private final Map stateTransitionFromOpenToHalfOpen = new HashMap<>(); + + private static final int TOTAL_NUMBER_CIRCUIT_BREAKERS = 10; + + @Before + public void setUp() { + CircuitBreakerConfig circuitBreakerConfigGroupA = CircuitBreakerConfig.custom() + .failureRateThreshold(50) + .ringBufferSizeInClosedState(5) + .ringBufferSizeInHalfOpenState(3) + .enableAutomaticTransitionFromOpenToHalfOpen() + .waitDurationInOpenState(Duration.ofSeconds(2)) + .recordFailure(error -> !(error instanceof NumberFormatException)) + .build(); + + CircuitBreakerConfig circuitBreakerConfigGroupB = CircuitBreakerConfig.custom() + .failureRateThreshold(50) + .ringBufferSizeInClosedState(5) + .ringBufferSizeInHalfOpenState(3) + .enableAutomaticTransitionFromOpenToHalfOpen() + .waitDurationInOpenState(Duration.ofSeconds(1)) + .recordFailure(error -> !(error instanceof NumberFormatException)) + .build(); + + // Instantiate multiple circuit breakers in two groups, A & B + for (int i = 0; i < TOTAL_NUMBER_CIRCUIT_BREAKERS; i++) { + + stateTransitionFromOpenToHalfOpen.put(i, 0); + // On state transition from OPEN to HALF_OPEN, increment a count + int finalI = i; + EventConsumer eventConsumer = transition -> { + if (transition.getStateTransition().getFromState().equals(CircuitBreaker.State.OPEN) && + transition.getStateTransition().getToState().equals(CircuitBreaker.State.HALF_OPEN)) { + Integer currentCount = stateTransitionFromOpenToHalfOpen.get(finalI); + stateTransitionFromOpenToHalfOpen.put(finalI, currentCount + 1); + } + }; + + CircuitBreaker circuitBreaker; + if (i < TOTAL_NUMBER_CIRCUIT_BREAKERS / 2) { + circuitBreaker = new CircuitBreakerStateMachine("testNameA" + i, circuitBreakerConfigGroupA); + circuitBreaker.getEventPublisher().onStateTransition(eventConsumer); + circuitBreakersGroupA.add(circuitBreaker); + } else { + circuitBreaker = new CircuitBreakerStateMachine("testNameB" + i, circuitBreakerConfigGroupB); + circuitBreaker.getEventPublisher().onStateTransition(eventConsumer); + circuitBreakersGroupB.add(circuitBreaker); + } + } + } + + @Test + public void testAutoTransition() throws InterruptedException { + // A ring buffer with size 5 is used in closed state + // Initially the CircuitBreakers are closed + this.assertAllGroupACircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupACircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + assertThatAllGroupAMetricsAreReset(); + + // Call 1 is a failure + circuitBreakersGroupA.forEach(cb -> cb.onError(0, new RuntimeException())); // Should create a CircuitBreakerOnErrorEvent (1) + this.assertAllGroupACircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupACircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + this.assertAllGroupAMetricsEqualTo(-1f, null, 1, null, 1, 0L); + + // Call 2 is a failure + circuitBreakersGroupA.forEach(cb -> cb.onError(0, new RuntimeException())); // Should create a CircuitBreakerOnErrorEvent (2) + this.assertAllGroupACircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupACircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + this.assertAllGroupAMetricsEqualTo(-1f, null, 2, null, 2, 0L); + + // Call 3 is a failure + circuitBreakersGroupA.forEach(cb -> cb.onError(0, new RuntimeException())); // Should create a CircuitBreakerOnErrorEvent (3) + this.assertAllGroupACircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupACircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + this.assertAllGroupAMetricsEqualTo(-1f, null, 3, null, 3, 0L); + + // Call 4 is a success + circuitBreakersGroupA.forEach(cb -> cb.onSuccess(0)); // Should create a CircuitBreakerOnSuccessEvent (4) + this.assertAllGroupACircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupACircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + this.assertAllGroupAMetricsEqualTo(-1f, null, 4, null, 3, 0L); + + // Call 5 is a success + circuitBreakersGroupA.forEach(cb -> cb.onSuccess(0)); // Should create a CircuitBreakerOnSuccessEvent (4) + // The ring buffer is filled and the failure rate is above 50% + this.assertAllGroupACircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.OPEN); // Should create a CircuitBreakerOnStateTransitionEvent (6) + this.assertAllGroupACircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getNumberOfBufferedCalls(), 5); + this.assertAllGroupACircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getNumberOfFailedCalls(), 3); + this.assertAllGroupACircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getFailureRate(), 60.0f); + this.assertAllGroupAMetricsEqualTo(60.0f, null, 5, null, 3, 0L); + + sleep(50); + + // Initially the CircuitBreakers are closed + this.assertAllGroupBCircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupBCircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + assertThatAllGroupBMetricsAreReset(); + + // Call 1 is a failure + circuitBreakersGroupB.forEach(cb -> cb.onError(0, new RuntimeException())); // Should create a CircuitBreakerOnErrorEvent (1) + this.assertAllGroupBCircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupBCircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + this.assertAllGroupBMetricsEqualTo(-1f, null, 1, null, 1, 0L); + + // Call 2 is a failure + circuitBreakersGroupB.forEach(cb -> cb.onError(0, new RuntimeException())); // Should create a CircuitBreakerOnErrorEvent (2) + this.assertAllGroupBCircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupBCircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + this.assertAllGroupBMetricsEqualTo(-1f, null, 2, null, 2, 0L); + + // Call 3 is a failure + circuitBreakersGroupB.forEach(cb -> cb.onError(0, new RuntimeException())); // Should create a CircuitBreakerOnErrorEvent (3) + this.assertAllGroupBCircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupBCircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + this.assertAllGroupBMetricsEqualTo(-1f, null, 3, null, 3, 0L); + + // Call 4 is a success + circuitBreakersGroupB.forEach(cb -> cb.onSuccess(0)); // Should create a CircuitBreakerOnSuccessEvent (4) + this.assertAllGroupBCircuitBreakers(CircuitBreaker::isCallPermitted, true); + this.assertAllGroupBCircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.CLOSED); + this.assertAllGroupBMetricsEqualTo(-1f, null, 4, null, 3, 0L); + + // Call 5 is a success + circuitBreakersGroupB.forEach(cb -> cb.onSuccess(0)); // Should create a CircuitBreakerOnSuccessEvent (4) + // The ring buffer is filled and the failure rate is above 50% + this.assertAllGroupBCircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.OPEN); // Should create a CircuitBreakerOnStateTransitionEvent (6) + this.assertAllGroupBCircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getNumberOfBufferedCalls(), 5); + this.assertAllGroupBCircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getNumberOfFailedCalls(), 3); + this.assertAllGroupBCircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getFailureRate(), 60.0f); + this.assertAllGroupBMetricsEqualTo(60.0f, null, 5, null, 3, 0L); + + sleep(400); + + // The CircuitBreakers in group A are still open, because the wait duration of 2 seconds is not elapsed + this.assertAllGroupACircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.OPEN); + this.assertAllGroupACircuitBreakers(CircuitBreaker::isCallPermitted, false); // Should create a CircuitBreakerOnCallNotPermittedEvent (7) + this.assertAllGroupACircuitBreakers(CircuitBreaker::isCallPermitted, false); // Should create a CircuitBreakerOnCallNotPermittedEvent (8) + // Two calls are tried, but not permitted, because the CircuitBreakers are open + this.assertAllGroupACircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getNumberOfNotPermittedCalls(), 2L); + + // The CircuitBreakers in group B are still open, because the wait duration of 1 second is not elapsed + this.assertAllGroupBCircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.OPEN); + this.assertAllGroupBCircuitBreakers(CircuitBreaker::isCallPermitted, false); // Should create a CircuitBreakerOnCallNotPermittedEvent (7) + this.assertAllGroupBCircuitBreakers(CircuitBreaker::isCallPermitted, false); // Should create a CircuitBreakerOnCallNotPermittedEvent (8) + // Two calls are tried, but not permitted, because the CircuitBreakers are open + this.assertAllGroupBCircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getNumberOfNotPermittedCalls(), 2L); + + sleep(650); + + // The CircuitBreakers in group A are still open, because the wait duration of 2 seconds is not elapsed + this.assertAllGroupACircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.OPEN); + this.assertAllGroupACircuitBreakers(CircuitBreaker::isCallPermitted, false); // Should create a CircuitBreakerOnCallNotPermittedEvent (9) + this.assertAllGroupACircuitBreakers(CircuitBreaker::isCallPermitted, false); // Should create a CircuitBreakerOnCallNotPermittedEvent (10) + // Two calls are tried, but not permitted, because the CircuitBreakers are open + this.assertAllGroupACircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getNumberOfNotPermittedCalls(), 4L); + + // The CircuitBreakers in Group B switch to half open, because the wait duration of 1 second is elapsed + this.assertAllGroupBCircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.HALF_OPEN); + assertThat(stateTransitionFromOpenToHalfOpen.values().stream().filter(count -> count.equals(1)).count()).isEqualTo((long) TOTAL_NUMBER_CIRCUIT_BREAKERS / 2); + // Metrics are reset + this.assertAllGroupBCircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getNumberOfFailedCalls(), 0); + this.assertAllGroupBMetricsEqualTo(-1f, null, 0, 3, 0, 0L); + + sleep(1000); + + // The CircuitBreakers switch to half open, because the wait duration of 2 second is elapsed + this.assertAllGroupACircuitBreakers(CircuitBreaker::getState, CircuitBreaker.State.HALF_OPEN); + assertThat(stateTransitionFromOpenToHalfOpen.values().stream().allMatch(count -> count.equals(1))).isEqualTo(true); + // Metrics are reset + this.assertAllGroupACircuitBreakers((CircuitBreaker cb) -> cb.getMetrics().getNumberOfFailedCalls(), 0); + this.assertAllGroupAMetricsEqualTo(-1f, null, 0, 3, 0, 0L); + } + + private void assertAllGroupACircuitBreakers(Function circuitBreakerFunction, Object expected) { + assertAllCircuitBreakers(circuitBreakersGroupA, circuitBreakerFunction, expected); + } + + private void assertAllGroupBCircuitBreakers(Function circuitBreakerFunction, Object expected) { + assertAllCircuitBreakers(circuitBreakersGroupB, circuitBreakerFunction, expected); + } + + private void assertAllCircuitBreakers(List circuitBreakers, Function circuitBreakerFunction, Object expected) { + circuitBreakers.forEach(circuitBreaker -> { + Object result = circuitBreakerFunction.apply(circuitBreaker); + assertThat(result).isEqualTo(expected); + }); + } + + private void assertAllGroupAMetricsEqualTo(Float expectedFailureRate, + Integer expectedSuccessCalls, + Integer expectedBufferedCalls, + Integer expectedMaxBufferedCalls, + Integer expectedFailedCalls, + Long expectedNotPermittedCalls) { + assertCircuitBreakerMetricsEqualTo(circuitBreakersGroupA, expectedFailureRate, expectedSuccessCalls, + expectedBufferedCalls, expectedMaxBufferedCalls, expectedFailedCalls, expectedNotPermittedCalls); + } + + private void assertAllGroupBMetricsEqualTo(Float expectedFailureRate, + Integer expectedSuccessCalls, + Integer expectedBufferedCalls, + Integer expectedMaxBufferedCalls, + Integer expectedFailedCalls, + Long expectedNotPermittedCalls) { + assertCircuitBreakerMetricsEqualTo(circuitBreakersGroupB, expectedFailureRate, expectedSuccessCalls, + expectedBufferedCalls, expectedMaxBufferedCalls, expectedFailedCalls, expectedNotPermittedCalls); + } + + private void assertCircuitBreakerMetricsEqualTo(List circuitBreakers, + Float expectedFailureRate, + Integer expectedSuccessCalls, + Integer expectedBufferedCalls, + Integer expectedMaxBufferedCalls, + Integer expectedFailedCalls, + Long expectedNotPermittedCalls) { + circuitBreakers.forEach(circuitBreaker -> { + final CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics(); + if (expectedFailureRate != null) { + assertThat(metrics.getFailureRate()).isEqualTo(expectedFailureRate); + } + if (expectedSuccessCalls != null) { + assertThat(metrics.getNumberOfSuccessfulCalls()).isEqualTo(expectedSuccessCalls); + } + if (expectedBufferedCalls != null) { + assertThat(metrics.getNumberOfBufferedCalls()).isEqualTo(expectedBufferedCalls); + } + if (expectedMaxBufferedCalls != null) { + assertThat(metrics.getMaxNumberOfBufferedCalls()).isEqualTo(expectedMaxBufferedCalls); + } + if (expectedFailedCalls != null) { + assertThat(metrics.getNumberOfFailedCalls()).isEqualTo(expectedFailedCalls); + } + if (expectedNotPermittedCalls != null) { + assertThat(metrics.getNumberOfNotPermittedCalls()).isEqualTo(expectedNotPermittedCalls); + } + }); + } + + private void assertThatAllGroupAMetricsAreReset() { + this.assertAllGroupAMetricsEqualTo(-1f, 0, 0, null, 0, 0L); + } + private void assertThatAllGroupBMetricsAreReset() { + this.assertAllGroupBMetricsEqualTo(-1f, 0, 0, null, 0, 0L); + } + +}