/* * Copyright 2017 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.http; import static com.linecorp.armeria.common.util.Functions.voidFunction; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import com.linecorp.armeria.client.AllInOneClientFactory; import com.linecorp.armeria.client.ClientFactory; import com.linecorp.armeria.client.Clients; import com.linecorp.armeria.client.SessionOption; import com.linecorp.armeria.client.SessionOptions; import com.linecorp.armeria.common.MediaType; import com.linecorp.armeria.common.http.HttpRequest; import com.linecorp.armeria.common.http.HttpResponse; import com.linecorp.armeria.common.http.HttpResponseWriter; import com.linecorp.armeria.common.http.HttpStatus; import com.linecorp.armeria.server.ServerBuilder; import com.linecorp.armeria.server.ServiceRequestContext; import com.linecorp.armeria.server.http.AbstractHttpService; import com.linecorp.armeria.testing.server.ServerRule; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; public class HttpClientPipeliningTest { // Server-side configuration private static final Semaphore semaphore = new Semaphore(0); private static final Lock lock = new ReentrantLock(); private static final Condition condition = lock.newCondition(); private static volatile boolean connectionReturnedToPool; @ClassRule public static final ServerRule server = new ServerRule() { @Override protected void configure(ServerBuilder sb) throws Exception { // Bind a service that returns the remote address of the connection to determine // if the same connection was used to handle more than one request. sb.serviceAt("/", new AbstractHttpService() { @Override protected void doGet(ServiceRequestContext ctx, HttpRequest req, HttpResponseWriter res) throws Exception { // Consume the request completely so that the connection can be returned to the pool. req.aggregate().handle(voidFunction((unused1, unused2) -> { // Signal the main thread that the connection has been returned to the pool. // Note that this is true only when pipelining is enabled. The connection is returned // after response is fully sent if pipelining is disabled. lock.lock(); try { connectionReturnedToPool = true; condition.signal(); } finally { lock.unlock(); } semaphore.acquireUninterruptibly(); try { res.respond(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, String.valueOf(ctx.remoteAddress())); } finally { semaphore.release(); } })); } }); } }; // Client-side configuration private static EventLoopGroup eventLoopGroup; private static ClientFactory factoryWithPipelining; private static ClientFactory factoryWithoutPipelining; @BeforeClass public static void initClientFactory() { // Ensure only a single event loop is used so that there's only one connection pool. // Note: Each event loop has its own connection pool. eventLoopGroup = new NioEventLoopGroup(1); factoryWithPipelining = new AllInOneClientFactory( SessionOptions.of(SessionOption.EVENT_LOOP_GROUP.newValue(eventLoopGroup), SessionOption.USE_HTTP1_PIPELINING.newValue(true))); factoryWithoutPipelining = new AllInOneClientFactory( SessionOptions.of(SessionOption.EVENT_LOOP_GROUP.newValue(eventLoopGroup), SessionOption.USE_HTTP1_PIPELINING.newValue(false))); } @AfterClass public static void destroyClientFactory() { ForkJoinPool.commonPool().execute(() -> { factoryWithPipelining.close(); factoryWithoutPipelining.close(); eventLoopGroup.shutdownGracefully(); }); } @Before public void resetState() { semaphore.drainPermits(); connectionReturnedToPool = false; } @Test public void withoutPipelining() throws Exception { final HttpClient client = Clients.newClient( factoryWithoutPipelining, "none+h1c://127.0.0.1:" + server.httpPort(), HttpClient.class); final HttpResponse res1 = client.get("/"); final HttpResponse res2 = client.get("/"); // At this point, the two requests have acquired two different connections from the pool // because pipelining is disabled and thus the connection of the first request will not // be returned to the pool until its response is fully received. // Give two permits to the Semaphore so that the two requests get their responses. semaphore.release(2); // Two requests should go through two different connections. final String remoteAddress1 = res1.aggregate().get().content().toStringUtf8(); final String remoteAddress2 = res2.aggregate().get().content().toStringUtf8(); assertThat(remoteAddress1).isNotEqualTo(remoteAddress2); } @Test public void withPipelining() throws Exception { final HttpClient client = Clients.newClient( factoryWithPipelining, "none+h1c://127.0.0.1:" + server.httpPort(), HttpClient.class); final HttpResponse res1; lock.lock(); try { res1 = client.get("/"); // Wait until the connection used by res1 is returned to the pool, // so that the next request reuses the connection. while (!connectionReturnedToPool) { condition.await(); } } finally { lock.unlock(); } // At this point, we are sure the connection of the first request has been returned to the pool, // because pipelining is enabled and thus the connection will be returned to the pool once // the request has been fully sent. // Now, send the second request so that the client reuses the connection. final HttpResponse res2 = client.get("/"); // Give two permits to the Semaphore so that the two requests get their responses. semaphore.release(2); // Two requests should go through one same connection. final String remoteAddress1 = res1.aggregate().get().content().toStringUtf8(); final String remoteAddress2 = res2.aggregate().get().content().toStringUtf8(); assertThat(remoteAddress1).isEqualTo(remoteAddress2); } }