/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you 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 com.linecorp.armeria.client.circuitbreaker; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import java.time.Duration; import org.junit.Test; import com.google.common.testing.FakeTicker; import com.linecorp.armeria.common.util.Exceptions; public class NonBlockingCircuitBreakerTest { private static final String remoteServiceName = "testService"; private static final FakeTicker ticker = new FakeTicker(); private static final Duration circuitOpenWindow = Duration.ofSeconds(1); private static final Duration trialRequestInterval = Duration.ofSeconds(1); private static final Duration counterUpdateInterval = Duration.ofSeconds(1); private static final CircuitBreakerListener listener = mock(CircuitBreakerListener.class); private static NonBlockingCircuitBreaker create(long minimumRequestThreshold, double failureRateThreshold) { return (NonBlockingCircuitBreaker) new CircuitBreakerBuilder(remoteServiceName) .failureRateThreshold(failureRateThreshold) .minimumRequestThreshold(minimumRequestThreshold) .circuitOpenWindow(circuitOpenWindow) .trialRequestInterval(trialRequestInterval) .counterSlidingWindow(Duration.ofSeconds(10)) .counterUpdateInterval(counterUpdateInterval) .listener(listener) .ticker(ticker) .build(); } private static CircuitBreaker closedState(long minimumRequestThreshold, double failureRateThreshold) { NonBlockingCircuitBreaker cb = create(minimumRequestThreshold, failureRateThreshold); assertThat(cb.state().isClosed(), is(true)); assertThat(cb.canRequest(), is(true)); return cb; } private static NonBlockingCircuitBreaker openState(long minimumRequestThreshold, double failureRateThreshold) { NonBlockingCircuitBreaker cb = create(minimumRequestThreshold, failureRateThreshold); cb.onSuccess(); cb.onFailure(); cb.onFailure(); ticker.advance(counterUpdateInterval.toNanos()); cb.onFailure(); assertThat(cb.state().isOpen(), is(true)); assertThat(cb.canRequest(), is(false)); return cb; } private static NonBlockingCircuitBreaker halfOpenState(long minimumRequestThreshold, double failureRateThreshold) { NonBlockingCircuitBreaker cb = openState(minimumRequestThreshold, failureRateThreshold); ticker.advance(circuitOpenWindow.toNanos()); assertThat(cb.state().isHalfOpen(), is(false)); assertThat(cb.canRequest(), is(true)); // first request is allowed assertThat(cb.state().isHalfOpen(), is(true)); assertThat(cb.canRequest(), is(false)); // seconds request is refused return cb; } @Test public void testClosed() { closedState(2, 0.5); } @Test public void testMinimumRequestThreshold() { NonBlockingCircuitBreaker cb = create(4, 0.5); assertThat(cb.state().isClosed() && cb.canRequest(), is(true)); cb.onFailure(); ticker.advance(counterUpdateInterval.toNanos()); cb.onFailure(); assertThat(cb.state().isClosed(), is(true)); assertThat(cb.canRequest(), is(true)); cb.onFailure(); cb.onFailure(); ticker.advance(counterUpdateInterval.toNanos()); cb.onFailure(); assertThat(cb.state().isOpen(), is(true)); assertThat(cb.canRequest(), is(false)); } @Test public void testFailureRateThreshold() { NonBlockingCircuitBreaker cb = create(10, 0.5); for (int i = 0; i < 10; i++) { cb.onSuccess(); } for (int i = 0; i < 9; i++) { cb.onFailure(); } ticker.advance(counterUpdateInterval.toNanos()); cb.onFailure(); assertThat(cb.state().isClosed(), is(true)); // 10 vs 9 (0.47) assertThat(cb.canRequest(), is(true)); ticker.advance(counterUpdateInterval.toNanos()); cb.onFailure(); assertThat(cb.state().isClosed(), is(true)); // 10 vs 10 (0.5) assertThat(cb.canRequest(), is(true)); ticker.advance(counterUpdateInterval.toNanos()); cb.onFailure(); assertThat(cb.state().isOpen(), is(true)); // 10 vs 11 (0.52) assertThat(cb.canRequest(), is(false)); } @Test public void testClosedToOpen() { openState(2, 0.5); } @Test public void testOpenToHalfOpen() { halfOpenState(2, 0.5); } @Test public void testHalfOpenToClosed() { NonBlockingCircuitBreaker cb = halfOpenState(2, 0.5); cb.onSuccess(); assertThat(cb.state().isClosed(), is(true)); assertThat(cb.canRequest(), is(true)); } @Test public void testHalfOpenToOpen() { NonBlockingCircuitBreaker cb = halfOpenState(2, 0.5); cb.onFailure(); assertThat(cb.state().isOpen(), is(true)); assertThat(cb.canRequest(), is(false)); } @Test public void testHalfOpenRetryRequest() { NonBlockingCircuitBreaker cb = halfOpenState(2, 0.5); ticker.advance(trialRequestInterval.toNanos()); assertThat(cb.state().isHalfOpen(), is(true)); assertThat(cb.canRequest(), is(true)); // first request is allowed assertThat(cb.state().isHalfOpen(), is(true)); assertThat(cb.canRequest(), is(false)); // seconds request is refused } @Test public void testFailureOfExceptionFilter() { NonBlockingCircuitBreaker cb = (NonBlockingCircuitBreaker) new CircuitBreakerBuilder() .exceptionFilter(cause -> { throw Exceptions.clearTrace(new Exception("exception filter failed")); }) .ticker(ticker) .build(); cb.onFailure(new Exception()); } @Test public void testNotification() throws Exception { reset(listener); NonBlockingCircuitBreaker cb = create(4, 0.5); // Notify initial state verify(listener, times(1)).onEventCountUpdated(cb, EventCount.ZERO); verify(listener, times(1)).onStateChanged(cb, CircuitState.CLOSED); reset(listener); cb.onFailure(); ticker.advance(counterUpdateInterval.toNanos()); cb.onFailure(); // Notify updated event count verify(listener, times(1)).onEventCountUpdated(cb, new EventCount(0, 1)); reset(listener); // Notify circuit tripped cb.onFailure(); cb.onFailure(); ticker.advance(counterUpdateInterval.toNanos()); cb.onFailure(); verify(listener, times(1)).onEventCountUpdated(cb, EventCount.ZERO); verify(listener, times(1)).onStateChanged(cb, CircuitState.OPEN); reset(listener); // Notify request rejected cb.canRequest(); verify(listener, times(1)).onRequestRejected(cb); ticker.advance(circuitOpenWindow.toNanos()); // Notify half open cb.canRequest(); verify(listener, times(1)).onEventCountUpdated(cb, EventCount.ZERO); verify(listener, times(1)).onStateChanged(cb, CircuitState.HALF_OPEN); reset(listener); // Notify circuit closed cb.onSuccess(); verify(listener, times(1)).onEventCountUpdated(cb, EventCount.ZERO); verify(listener, times(1)).onStateChanged(cb, CircuitState.CLOSED); } }