@Test
  public void shouldNotEvictAccessedBucket() throws Exception {
    FakeTimeService time = new FakeTimeService(0);
    ThrottlingFilter filter =
        new ThrottlingFilter(time, 2, duration("3 seconds"), DEFAULT_PARTITION_EXPR);
    Context context = mock(Context.class);
    Request request;

    // Here we access the same bucket every 2 seconds : enough time to add a refilled token but not
    // enough for the
    // bucket to be evicted.
    Handler handler1 = mock(Handler.class, "handler1");
    request = new Request();
    filter.filter(context, request, handler1);
    verify(handler1).handle(context, request);

    time.advance(duration("2 seconds"));
    Handler handler2 = mock(Handler.class, "handler2");
    request = new Request();
    filter.filter(context, request, handler2);
    verify(handler2).handle(context, request);
    assertThat(filter.getBucketsStats().evictionCount()).isEqualTo(0);

    time.advance(duration("2 seconds"));
    Handler handler3 = mock(Handler.class, "handler3");
    request = new Request();
    filter.filter(context, request, handler3);
    verify(handler3).handle(context, request);
    assertThat(filter.getBucketsStats().evictionCount()).isEqualTo(0);
  }
  @Test
  public void shouldThrottleRequests() throws Exception {
    FakeTimeService time = new FakeTimeService(0);
    ThrottlingFilter filter =
        new ThrottlingFilter(time, 1, duration("3 seconds"), DEFAULT_PARTITION_EXPR);

    // This one has to call the handler as there are enough tokens in the bucket.
    Handler handler1 = mock(Handler.class, "handler1");
    filter.filter(mock(Context.class), new Request(), handler1);
    verify(handler1).handle(any(Context.class), any(Request.class));

    time.advance(duration("2 seconds"));
    // This one does not have to be called as there is no token anymore in the bucket.
    Handler handler2 = mock(Handler.class, "handler2");
    Promise<Response, NeverThrowsException> promise2 =
        filter.filter(mock(Context.class), new Request(), handler2);
    verifyZeroInteractions(handler2);
    assertThat(promise2.get().getStatus()).isEqualTo(Status.TOO_MANY_REQUESTS);

    time.advance(duration("4 seconds"));
    // This one has to call the handler as the bucket has been refilled.
    Handler handler3 = mock(Handler.class, "handler3");
    filter.filter(mock(Context.class), new Request(), handler3);
    verify(handler3).handle(any(Context.class), any(Request.class));
  }
  @Test
  public void shouldThrottleConcurrentRequests() throws Exception {
    CountDownLatch latch1 = new CountDownLatch(1);
    CountDownLatch latch2 = new CountDownLatch(1);

    FakeTimeService time = new FakeTimeService(0);
    final ThrottlingFilter filter =
        new ThrottlingFilter(time, 1, duration("3 seconds"), DEFAULT_PARTITION_EXPR);

    // This one has to be called as there are enough tokens in the bucket.
    final Handler handler1 = new LatchHandler(latch1, latch2);

    Runnable r =
        new Runnable() {

          @Override
          public void run() {
            filter.filter(mock(Context.class), new Request(), handler1);
          }
        };
    Thread t1 = new Thread(r);
    t1.setName("Filter for request #1");
    t1.start();
    latch2.await();

    time.advance(duration("2 seconds"));
    try {
      // This one does not have to be called as there no token anymore in the bucket.
      Handler handler2 = mock(Handler.class, "handler2");
      Promise<Response, NeverThrowsException> promise2 =
          filter.filter(mock(Context.class), new Request(), handler2);

      verifyZeroInteractions(handler2);

      Response response = promise2.get(20, TimeUnit.SECONDS);
      assertThat(response.getStatus()).isEqualTo(Status.TOO_MANY_REQUESTS);
      assertThat(response.getHeaders().getFirst("Retry-After")).isEqualTo("1");
    } finally {
      latch1.countDown();
      t1.join();
    }
  }
  @Test
  public void shouldEvictObsoleteBucket() throws Exception {
    FakeTimeService time = new FakeTimeService(0);
    ThrottlingFilter filter =
        new ThrottlingFilter(time, 1, duration("3 seconds"), DEFAULT_PARTITION_EXPR);
    Context context = mock(Context.class);

    // Call the filter in order to trigger the bucket creation
    Handler handler1 = mock(Handler.class, "handler1");
    filter.filter(context, new Request(), handler1);
    verify(handler1).handle(any(Exchange.class), any(Request.class));

    time.advance(duration("4 seconds"));

    // After 3 seconds, the previously bucket should be discarded, force a new call so that the
    // cache will clean its
    // internal structure
    Handler handler2 = mock(Handler.class, "handler2");
    filter.filter(context, new Request(), handler2);
    verify(handler2).handle(any(Exchange.class), any(Request.class));

    assertThat(filter.getBucketsStats().evictionCount()).isEqualTo(1);
  }