@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);
    Future<Object> successFuture = successFuture();
    Future<Object> failedFuture = failedFuture();

    CircuitBreaker circuitBreaker =
        new CircuitBreakerBuilder(remoteServiceName)
            .minimumRequestThreshold(minimumRequestThreshold)
            .circuitOpenWindow(circuitOpenWindow)
            .counterSlidingWindow(counterSlidingWindow)
            .counterUpdateInterval(counterUpdateInterval)
            .ticker(ticker)
            .build();

    RemoteInvoker remoteInvoker = mock(RemoteInvoker.class);
    // return failed future
    when(remoteInvoker.invoke(any(), any(), any(), any(), any(), any())).thenReturn(failedFuture);

    CircuitBreakerMapping mapping =
        (eventLoop1, uri, options1, codec1, method, args1) -> circuitBreaker;
    CircuitBreakerRemoteInvoker stub = new CircuitBreakerRemoteInvoker(remoteInvoker, mapping);

    // CLOSED
    for (int i = 0; i < minimumRequestThreshold + 1; i++) {
      Future<Object> future = stub.invoke(eventLoop, uri, options, codec, methodA(), args);
      // The future is `failedFuture` itself
      assertThat(future.isSuccess(), is(false));
      // This is not a CircuitBreakerException
      assertThat(future.cause(), is(not(instanceOf(FailFastException.class))));
      ticker.advance(Duration.ofMillis(1).toNanos());
    }

    // OPEN
    Future<Object> future1 = stub.invoke(eventLoop, uri, options, codec, methodA(), args);
    // The circuit is OPEN
    assertThat(future1.isSuccess(), is(false));
    assertThat(future1.cause(), instanceOf(FailFastException.class));
    assertThat(((FailFastException) future1.cause()).getCircuitBreaker(), is(circuitBreaker));

    ticker.advance(circuitOpenWindow.toNanos());

    // return success future
    when(remoteInvoker.invoke(any(), any(), any(), any(), any(), any())).thenReturn(successFuture);

    // HALF OPEN
    Future<Object> future2 = stub.invoke(eventLoop, uri, options, codec, methodA(), args);
    assertThat(future2.isSuccess(), is(true));

    // CLOSED
    Future<Object> future3 = stub.invoke(eventLoop, uri, options, codec, methodA(), args);
    assertThat(future3.isSuccess(), is(true));
  }
  @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);
    Future<Object> successFuture = successFuture();
    Future<Object> failedFuture = failedFuture();

    Function<String, CircuitBreaker> factory =
        method ->
            new CircuitBreakerBuilder(remoteServiceName)
                .minimumRequestThreshold(minimumRequestThreshold)
                .circuitOpenWindow(circuitOpenWindow)
                .counterSlidingWindow(counterSlidingWindow)
                .counterUpdateInterval(counterUpdateInterval)
                .ticker(ticker)
                .build();

    RemoteInvoker remoteInvoker = mock(RemoteInvoker.class);
    // Always return failed future for methodA
    when(remoteInvoker.invoke(any(), any(), any(), any(), eq(methodA()), any()))
        .thenReturn(failedFuture);
    // Always return success future for methodB
    when(remoteInvoker.invoke(any(), any(), any(), any(), eq(methodB()), any()))
        .thenReturn(successFuture);

    CircuitBreakerMapping mapping =
        new KeyedCircuitBreakerMapping<>(KeySelector.METHOD, factory::apply);
    CircuitBreakerRemoteInvoker stub = new CircuitBreakerRemoteInvoker(remoteInvoker, mapping);

    // CLOSED (methodA)
    for (int i = 0; i < minimumRequestThreshold + 1; i++) {
      stub.invoke(eventLoop, uri, options, codec, methodA(), args);
      ticker.advance(Duration.ofMillis(1).toNanos());
    }

    // OPEN (methodA)
    Future<Object> future1 = stub.invoke(eventLoop, uri, options, codec, methodA(), args);
    assertThat(future1.isSuccess(), is(false));
    assertThat(future1.cause(), instanceOf(FailFastException.class));

    // CLOSED (methodB)
    Future<Object> future2 = stub.invoke(eventLoop, uri, options, codec, methodB(), args);
    assertThat(future2.isSuccess(), is(true));
  }
  @Test
  public void testDelegateRemoteInvocationIfFailToGetCircuitBreaker() throws Exception {
    Future<Object> successFuture = successFuture();

    RemoteInvoker remoteInvoker = mock(RemoteInvoker.class);
    when(remoteInvoker.invoke(any(), any(), any(), any(), any(), any())).thenReturn(successFuture);

    CircuitBreakerMapping mapping =
        (eventLoop1, uri, options1, codec1, method, args1) -> {
          throw new IllegalArgumentException();
        };
    CircuitBreakerRemoteInvoker stub = new CircuitBreakerRemoteInvoker(remoteInvoker, mapping);

    stub.invoke(eventLoop, uri, options, codec, methodA(), args);

    // make sure that remote service is invoked even if cb mapping is failed
    verify(remoteInvoker, times(1))
        .invoke(eq(eventLoop), eq(uri), eq(options), eq(codec), eq(methodA()), eq(args));
  }
  @Test
  public void testDelegateRemoteInvocation() throws Exception {
    FakeTicker ticker = new FakeTicker();
    Future<Object> successFuture = successFuture();

    CircuitBreaker circuitBreaker =
        new CircuitBreakerBuilder(remoteServiceName).ticker(ticker).build();

    RemoteInvoker remoteInvoker = mock(RemoteInvoker.class);
    when(remoteInvoker.invoke(any(), any(), any(), any(), any(), any())).thenReturn(successFuture);

    CircuitBreakerMapping mapping =
        (eventLoop1, uri, options1, codec1, method, args1) -> circuitBreaker;
    CircuitBreakerRemoteInvoker stub = new CircuitBreakerRemoteInvoker(remoteInvoker, mapping);

    stub.invoke(eventLoop, uri, options, codec, methodA(), args);

    verify(remoteInvoker, times(1))
        .invoke(eq(eventLoop), eq(uri), eq(options), eq(codec), eq(methodA()), eq(args));
  }