/* * 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.limit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.concurrent.TimeUnit; import org.junit.AfterClass; import org.junit.Test; import com.linecorp.armeria.client.Client; import com.linecorp.armeria.client.ClientRequestContext; import com.linecorp.armeria.client.ResponseTimeoutException; import com.linecorp.armeria.common.http.DefaultHttpResponse; import com.linecorp.armeria.common.http.DeferredHttpResponse; import com.linecorp.armeria.common.http.HttpRequest; import com.linecorp.armeria.common.http.HttpResponse; import com.linecorp.armeria.common.stream.NoopSubscriber; import io.netty.channel.DefaultEventLoop; import io.netty.channel.EventLoop; public class ConcurrencyLimitingHttpClientTest { private static final EventLoop eventLoop = new DefaultEventLoop(); @AfterClass public static void destroy() { eventLoop.shutdownGracefully(); } /** * Tests the request pattern that does not exceed maxConcurrency. */ @Test public void testOrdinaryRequest() throws Exception { final ClientRequestContext ctx = newContext(); final HttpRequest req = mock(HttpRequest.class); final DefaultHttpResponse actualRes = new DefaultHttpResponse(); @SuppressWarnings("unchecked") final Client<HttpRequest, HttpResponse> delegate = mock(Client.class); when(delegate.execute(ctx, req)).thenReturn(actualRes); final ConcurrencyLimitingHttpClient client = ConcurrencyLimitingHttpClient.newDecorator(1).apply(delegate); assertThat(client.numActiveRequests()).isZero(); final HttpResponse res = client.execute(ctx, req); assertThat(res).isInstanceOf(DeferredHttpResponse.class); assertThat(res.isOpen()).isTrue(); assertThat(client.numActiveRequests()).isEqualTo(1); closeAndDrain(actualRes, res); assertThat(res.isOpen()).isFalse(); assertThat(client.numActiveRequests()).isZero(); } /** * Tests the request pattern that exceeds maxConcurrency. */ @Test public void testLimitedRequest() throws Exception { final ClientRequestContext ctx1 = newContext(); final ClientRequestContext ctx2 = newContext(); final HttpRequest req1 = mock(HttpRequest.class); final HttpRequest req2 = mock(HttpRequest.class); final DefaultHttpResponse actualRes1 = new DefaultHttpResponse(); final DefaultHttpResponse actualRes2 = new DefaultHttpResponse(); @SuppressWarnings("unchecked") final Client<HttpRequest, HttpResponse> delegate = mock(Client.class); when(delegate.execute(ctx1, req1)).thenReturn(actualRes1); when(delegate.execute(ctx2, req2)).thenReturn(actualRes2); final ConcurrencyLimitingHttpClient client = ConcurrencyLimitingHttpClient.newDecorator(1).apply(delegate); // The first request should be delegated immediately. final HttpResponse res1 = client.execute(ctx1, req1); verify(delegate).execute(ctx1, req1); assertThat(res1).isInstanceOf(DeferredHttpResponse.class); assertThat(res1.isOpen()).isTrue(); // The second request should never be delegated until the first response is closed. final HttpResponse res2 = client.execute(ctx2, req2); verify(delegate, never()).execute(ctx2, req2); assertThat(res2).isInstanceOf(DeferredHttpResponse.class); assertThat(res2.isOpen()).isTrue(); assertThat(client.numActiveRequests()).isEqualTo(1); // Only req1 is active. // Complete res1. closeAndDrain(actualRes1, res1); // Once res1 is complete, req2 should be delegated. verify(delegate).execute(ctx2, req2); assertThat(client.numActiveRequests()).isEqualTo(1); // Only req2 is active. // Complete res2, leaving no active requests. closeAndDrain(actualRes2, res2); assertThat(client.numActiveRequests()).isZero(); } /** * Tests if the request is not delegated but closed when the timeout is reached before delegation. */ @Test public void testTimeout() throws Exception { final ClientRequestContext ctx1 = newContext(); final ClientRequestContext ctx2 = newContext(); final HttpRequest req1 = mock(HttpRequest.class); final HttpRequest req2 = mock(HttpRequest.class); final DefaultHttpResponse actualRes1 = new DefaultHttpResponse(); final DefaultHttpResponse actualRes2 = new DefaultHttpResponse(); @SuppressWarnings("unchecked") final Client<HttpRequest, HttpResponse> delegate = mock(Client.class); when(delegate.execute(ctx1, req1)).thenReturn(actualRes1); when(delegate.execute(ctx2, req2)).thenReturn(actualRes2); final ConcurrencyLimitingHttpClient client = ConcurrencyLimitingHttpClient.newDecorator(1, 500, TimeUnit.MILLISECONDS).apply(delegate); // Send two requests, where only the first one is delegated. final HttpResponse res1 = client.execute(ctx1, req1); final HttpResponse res2 = client.execute(ctx2, req2); // Let req2 time out. Thread.sleep(1000); res2.subscribe(NoopSubscriber.get()); assertThat(res2.isOpen()).isFalse(); assertThat(res2.closeFuture()).isCompletedExceptionally(); assertThatThrownBy(() -> res2.closeFuture().get()).hasCauseInstanceOf(ResponseTimeoutException.class); // req1 should not time out because it's been delegated already. res1.subscribe(NoopSubscriber.get()); assertThat(res1.isOpen()).isTrue(); assertThat(res1.closeFuture()).isNotDone(); // Close req1 and make sure req2 does not affect numActiveRequests. actualRes1.close(); waitForEventLoop(); assertThat(client.numActiveRequests()).isZero(); } /** * Tests the case where a delegate raises an exception rather than returning a response. */ @Test public void testFaultyDelegate() throws Exception { final ClientRequestContext ctx = newContext(); final HttpRequest req = mock(HttpRequest.class); @SuppressWarnings("unchecked") final Client<HttpRequest, HttpResponse> delegate = mock(Client.class); when(delegate.execute(ctx, req)).thenThrow(Exception.class); final ConcurrencyLimitingHttpClient client = ConcurrencyLimitingHttpClient.newDecorator(1).apply(delegate); assertThat(client.numActiveRequests()).isZero(); final HttpResponse res = client.execute(ctx, req); // Consume everything from the returned response so its close future is completed. res.subscribe(NoopSubscriber.get()); assertThat(res).isInstanceOf(DeferredHttpResponse.class); assertThat(res.isOpen()).isFalse(); assertThat(res.closeFuture()).isCompletedExceptionally(); assertThatThrownBy(() -> res.closeFuture().get()).hasCauseInstanceOf(Exception.class); assertThat(client.numActiveRequests()).isZero(); } @Test public void testUnlimitedRequest() throws Exception { final ClientRequestContext ctx = newContext(); final HttpRequest req = mock(HttpRequest.class); final DefaultHttpResponse actualRes = new DefaultHttpResponse(); @SuppressWarnings("unchecked") final Client<HttpRequest, HttpResponse> delegate = mock(Client.class); when(delegate.execute(ctx, req)).thenReturn(actualRes); final ConcurrencyLimitingHttpClient client = ConcurrencyLimitingHttpClient.newDecorator(0).apply(delegate); // A request should be delegated immediately, creating no deferred response. final HttpResponse res = client.execute(ctx, req); verify(delegate).execute(ctx, req); assertThat(res).isNotInstanceOf(DeferredHttpResponse.class); assertThat(res.isOpen()).isTrue(); assertThat(client.numActiveRequests()).isEqualTo(1); // Complete the response, leaving no active requests. closeAndDrain(actualRes, res); assertThat(client.numActiveRequests()).isZero(); } @Test public void testUnlimitedRequestWithFaultyDelegate() throws Exception { final ClientRequestContext ctx = newContext(); final HttpRequest req = mock(HttpRequest.class); @SuppressWarnings("unchecked") final Client<HttpRequest, HttpResponse> delegate = mock(Client.class); when(delegate.execute(ctx, req)).thenThrow(Exception.class); final ConcurrencyLimitingHttpClient client = ConcurrencyLimitingHttpClient.newDecorator(0).apply(delegate); // A request should be delegated immediately, rethrowing the exception from the delegate. assertThatThrownBy(() -> client.execute(ctx, req)).isInstanceOf(Exception.class); verify(delegate).execute(ctx, req); // The number of active requests should increase and then immediately decrease. i.e. stay back at 0. assertThat(client.numActiveRequests()).isZero(); } private static ClientRequestContext newContext() { final ClientRequestContext ctx = mock(ClientRequestContext.class); when(ctx.eventLoop()).thenReturn(eventLoop); return ctx; } /** * Closes the response returned by the delegate and consumes everything from it, so that its close future * is completed. */ private static void closeAndDrain(DefaultHttpResponse actualRes, HttpResponse deferredRes) { actualRes.close(); deferredRes.subscribe(NoopSubscriber.get()); waitForEventLoop(); } private static void waitForEventLoop() { eventLoop.submit(() -> { /* no-op */ }).syncUninterruptibly(); } }