/* * 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 com.linecorp.armeria.common.http.HttpSessionProtocols.H2C; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.time.Duration; import java.util.function.Function; import org.junit.Test; import com.google.common.testing.FakeTicker; import com.linecorp.armeria.client.Client; import com.linecorp.armeria.client.ClientOptions; import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.DefaultClientRequestContext; import com.linecorp.armeria.client.Endpoint; import com.linecorp.armeria.client.circuitbreaker.KeyedCircuitBreakerMapping.KeySelector; import com.linecorp.armeria.common.RpcRequest; import com.linecorp.armeria.common.RpcResponse; import com.linecorp.armeria.common.util.Exceptions; import io.netty.channel.DefaultEventLoop; public class CircuitBreakerClientTest { private static final String remoteServiceName = "testService"; // Remote invocation parameters private static final ClientRequestContext ctx = new DefaultClientRequestContext( new DefaultEventLoop(), H2C, Endpoint.of("dummyhost", 8080), "POST", "/", "", ClientOptions.DEFAULT, RpcRequest.of(Object.class, "methodA", "a", "b")); private static final ClientRequestContext ctxB = new DefaultClientRequestContext( new DefaultEventLoop(), H2C, Endpoint.of("dummyhost", 8080), "POST", "/", "", ClientOptions.DEFAULT, RpcRequest.of(Object.class, "methodB", "c", "d")); private static final RpcRequest req = ctx.request(); private static final RpcRequest reqB = ctxB.request(); private static final RpcResponse successRes = RpcResponse.of(null); private static final RpcResponse failureRes = RpcResponse.ofFailure( Exceptions.clearTrace(new Exception("bug"))); @Test public void testSingletonDecorator() throws Exception { CircuitBreaker circuitBreaker = mock(CircuitBreaker.class); final int COUNT = 1; failFastInvocation(circuitBreaker, CircuitBreakerClient.newDecorator(circuitBreaker), COUNT); verify(circuitBreaker, times(COUNT)).canRequest(); } @Test public void testPerMethodDecorator() throws Exception { CircuitBreaker circuitBreaker = mock(CircuitBreaker.class); @SuppressWarnings("unchecked") Function<String, CircuitBreaker> factory = (Function<String, CircuitBreaker>) mock(Function.class); when(factory.apply(any())).thenReturn(circuitBreaker); final int COUNT = 2; failFastInvocation(circuitBreaker, CircuitBreakerClient.newPerMethodDecorator(factory), COUNT); verify(circuitBreaker, times(COUNT)).canRequest(); verify(factory, times(1)).apply("methodA"); } @Test public void testPerHostDecorator() throws Exception { CircuitBreaker circuitBreaker = mock(CircuitBreaker.class); @SuppressWarnings("unchecked") Function<String, CircuitBreaker> factory = (Function<String, CircuitBreaker>) mock(Function.class); when(factory.apply(any())).thenReturn(circuitBreaker); final int COUNT = 2; failFastInvocation(circuitBreaker, CircuitBreakerClient.newPerHostDecorator(factory), COUNT); verify(circuitBreaker, times(COUNT)).canRequest(); verify(factory, times(1)).apply("dummyhost:8080"); } @Test public void testPerHostAndMethodDecorator() throws Exception { CircuitBreaker circuitBreaker = mock(CircuitBreaker.class); @SuppressWarnings("unchecked") Function<String, CircuitBreaker> factory = (Function<String, CircuitBreaker>) mock(Function.class); when(factory.apply(any())).thenReturn(circuitBreaker); final int COUNT = 2; failFastInvocation(circuitBreaker, CircuitBreakerClient.newPerHostAndMethodDecorator(factory), COUNT); verify(circuitBreaker, times(COUNT)).canRequest(); verify(factory, times(1)).apply("dummyhost:8080#methodA"); } @Test public void testDelegate() throws Exception { FakeTicker ticker = new FakeTicker(); CircuitBreaker circuitBreaker = new CircuitBreakerBuilder(remoteServiceName) .ticker(ticker) .build(); @SuppressWarnings("unchecked") Client<RpcRequest, RpcResponse> delegate = mock(Client.class); when(delegate.execute(any(), any())).thenReturn(successRes); CircuitBreakerMapping mapping = (ctx, req) -> circuitBreaker; CircuitBreakerClient<RpcRequest, RpcResponse> stub = new CircuitBreakerClient<>(delegate, mapping); stub.execute(ctx, req); verify(delegate, times(1)).execute(eq(ctx), eq(req)); } @Test public void testDelegateIfFailToGetCircuitBreaker() throws Exception { @SuppressWarnings("unchecked") Client<RpcRequest, RpcResponse> delegate = mock(Client.class); when(delegate.execute(any(), any())).thenReturn(successRes); CircuitBreakerMapping mapping = (ctx, req) -> { throw Exceptions.clearTrace(new IllegalArgumentException("bug!")); }; CircuitBreakerClient<RpcRequest, RpcResponse> stub = new CircuitBreakerClient<>(delegate, mapping); stub.execute(ctx, req); // make sure that remote service is invoked even if cb mapping is failed verify(delegate, times(1)).execute(eq(ctx), eq(req)); } @Test public void testStateTransition() throws Exception { FakeTicker ticker = new FakeTicker(); int minimumRequestThreshold = 2; Duration circuitOpenWindow = Duration.ofSeconds(60); Duration counterSlidingWindow = Duration.ofSeconds(180); Duration counterUpdateInterval = Duration.ofMillis(1); CircuitBreaker circuitBreaker = new CircuitBreakerBuilder(remoteServiceName) .minimumRequestThreshold(minimumRequestThreshold) .circuitOpenWindow(circuitOpenWindow) .counterSlidingWindow(counterSlidingWindow) .counterUpdateInterval(counterUpdateInterval) .ticker(ticker) .build(); @SuppressWarnings("unchecked") Client<RpcRequest, RpcResponse> delegate = mock(Client.class); // return failed future when(delegate.execute(ctx, req)).thenReturn(failureRes); CircuitBreakerMapping mapping = (ctx, req) -> circuitBreaker; CircuitBreakerClient<RpcRequest, RpcResponse> stub = new CircuitBreakerClient<>(delegate, mapping); // CLOSED for (int i = 0; i < minimumRequestThreshold + 1; i++) { RpcResponse future = stub.execute(ctx, req); // The future is `failureRes` itself assertThat(future.isCompletedExceptionally(), is(true)); // This is not a CircuitBreakerException assertThat(future.cause(), is(not(instanceOf(FailFastException.class)))); ticker.advance(Duration.ofMillis(1).toNanos()); } // OPEN try { stub.execute(ctx, req); fail(); } catch (FailFastException e) { // The circuit is OPEN assertThat(e.getCircuitBreaker(), is(circuitBreaker)); } ticker.advance(circuitOpenWindow.toNanos()); // return success future when(delegate.execute(ctx, req)).thenReturn(successRes); // HALF OPEN RpcResponse future2 = stub.execute(ctx, req); assertThat(future2.get(), is(nullValue())); // CLOSED RpcResponse future3 = stub.execute(ctx, req); assertThat(future3.get(), is(nullValue())); } @Test public void testServiceScope() throws Exception { FakeTicker ticker = new FakeTicker(); int minimumRequestThreshold = 2; Duration circuitOpenWindow = Duration.ofSeconds(60); Duration counterSlidingWindow = Duration.ofSeconds(180); Duration counterUpdateInterval = Duration.ofMillis(1); CircuitBreaker circuitBreaker = new CircuitBreakerBuilder(remoteServiceName) .minimumRequestThreshold(minimumRequestThreshold) .circuitOpenWindow(circuitOpenWindow) .counterSlidingWindow(counterSlidingWindow) .counterUpdateInterval(counterUpdateInterval) .ticker(ticker) .build(); @SuppressWarnings("unchecked") Client<RpcRequest, RpcResponse> delegate = mock(Client.class); // Always return failed future for methodA when(delegate.execute(ctx, req)).thenReturn(failureRes); // Always return success future for methodB when(delegate.execute(ctxB, reqB)).thenReturn(successRes); CircuitBreakerMapping mapping = (ctx, req) -> circuitBreaker; CircuitBreakerClient<RpcRequest, RpcResponse> stub = new CircuitBreakerClient<>(delegate, mapping); // CLOSED for (int i = 0; i < minimumRequestThreshold + 1; i++) { stub.execute(ctx, req); ticker.advance(Duration.ofMillis(1).toNanos()); } // OPEN (methodA) try { stub.execute(ctx, req); fail(); } catch (FailFastException e) { // Expected } // OPEN (methodB) try { stub.execute(ctxB, reqB); fail(); } catch (FailFastException e) { // Expected } } @Test public void testPerMethodScope() throws Exception { FakeTicker ticker = new FakeTicker(); int minimumRequestThreshold = 2; Duration circuitOpenWindow = Duration.ofSeconds(60); Duration counterSlidingWindow = Duration.ofSeconds(180); Duration counterUpdateInterval = Duration.ofMillis(1); Function<String, CircuitBreaker> factory = method -> new CircuitBreakerBuilder(remoteServiceName) .minimumRequestThreshold(minimumRequestThreshold) .circuitOpenWindow(circuitOpenWindow) .counterSlidingWindow(counterSlidingWindow) .counterUpdateInterval(counterUpdateInterval) .ticker(ticker) .build(); @SuppressWarnings("unchecked") Client<RpcRequest, RpcResponse> delegate = mock(Client.class); // Always return failed future for methodA when(delegate.execute(ctx, req)).thenReturn(failureRes); // Always return success future for methodB when(delegate.execute(ctxB, reqB)).thenReturn(successRes); CircuitBreakerMapping mapping = new KeyedCircuitBreakerMapping<>(KeySelector.METHOD, factory); CircuitBreakerClient<RpcRequest, RpcResponse> stub = new CircuitBreakerClient<>(delegate, mapping); // CLOSED (methodA) for (int i = 0; i < minimumRequestThreshold + 1; i++) { try { stub.execute(ctx, req); assertThat(i, is(lessThanOrEqualTo(minimumRequestThreshold))); } catch (FailFastException e) { assertThat(i, is(greaterThan(minimumRequestThreshold))); } ticker.advance(Duration.ofMillis(1).toNanos()); } // OPEN (methodA) try { stub.execute(ctx, req); fail(); } catch (FailFastException e) { // Expected } // CLOSED (methodB) RpcResponse future2 = stub.execute(ctxB, reqB); assertThat(future2.get(), is(nullValue())); } @Test public void testExceptionFilter() throws Exception { FakeTicker ticker = new FakeTicker(); int minimumRequestThreshold = 2; Duration circuitOpenWindow = Duration.ofSeconds(60); Duration counterSlidingWindow = Duration.ofSeconds(180); Duration counterUpdateInterval = Duration.ofMillis(1); // a filter that ignores all exception ExceptionFilter exceptionFilter = cause -> false; CircuitBreaker circuitBreaker = new CircuitBreakerBuilder(remoteServiceName) .minimumRequestThreshold(minimumRequestThreshold) .circuitOpenWindow(circuitOpenWindow) .counterSlidingWindow(counterSlidingWindow) .counterUpdateInterval(counterUpdateInterval) .exceptionFilter(exceptionFilter) .ticker(ticker) .build(); @SuppressWarnings("unchecked") Client<RpcRequest, RpcResponse> delegate = mock(Client.class); // return failed future when(delegate.execute(ctx, req)).thenReturn(failureRes); CircuitBreakerMapping mapping = (ctx, req) -> circuitBreaker; CircuitBreakerClient<RpcRequest, RpcResponse> stub = new CircuitBreakerClient<>(delegate, mapping); // CLOSED for (int i = 0; i < minimumRequestThreshold + 1; i++) { RpcResponse future = stub.execute(ctx, req); // The future is `failedFuture` itself assertThat(future.isCompletedExceptionally(), is(true)); // This is not a CircuitBreakerException assertThat(future.cause(), is(not(instanceOf(FailFastException.class)))); ticker.advance(Duration.ofMillis(1).toNanos()); } // OPEN RpcResponse future1 = stub.execute(ctx, req); // The circuit is still CLOSED assertThat(future1.isCompletedExceptionally(), is(true)); assertThat(future1.cause(), is(not(instanceOf(FailFastException.class)))); } private static void invoke(Function<Client<? super RpcRequest, ? extends RpcResponse>, ? extends Client<RpcRequest, RpcResponse>> decorator) throws Exception { @SuppressWarnings("unchecked") Client<RpcRequest, RpcResponse> client = mock(Client.class); Client<RpcRequest, RpcResponse> decorated = decorator.apply(client); decorated.execute(ctx, req); } private static void failFastInvocation( CircuitBreaker circuitBreaker, Function<Client<? super RpcRequest, ? extends RpcResponse>, ? extends Client<RpcRequest, RpcResponse>> decorator, int count) throws Exception { for (int i = 0; i < count; i++) { try { invoke(decorator); fail(); } catch (FailFastException e) { assertThat(e.getCircuitBreaker(), is(circuitBreaker)); } } } }