Java tutorial
/* * Copyright 2014 Netflix, Inc. * * Licensed 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 io.reactivex.netty.protocol.http.client; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelPipeline; import io.netty.handler.logging.LogLevel; import io.netty.handler.timeout.ReadTimeoutException; import io.reactivex.netty.ChannelCloseListener; import io.reactivex.netty.RxNetty; import io.reactivex.netty.client.PoolConfig; import io.reactivex.netty.client.PoolStats; import io.reactivex.netty.client.TrackableMetricEventsListener; import io.reactivex.netty.metrics.HttpClientMetricEventsListener; import io.reactivex.netty.pipeline.PipelineConfigurator; import io.reactivex.netty.pipeline.PipelineConfiguratorComposite; import io.reactivex.netty.pipeline.PipelineConfigurators; import io.reactivex.netty.protocol.http.server.HttpServer; import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import rx.Observable; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * @author Nitesh Kant */ public class HttpClientPoolTest { private static HttpServer<ByteBuf, ByteBuf> mockServer; private static int port; private final ChannelCloseListener channelCloseListener = new ChannelCloseListener(); private HttpClientImpl<ByteBuf, ByteBuf> client; private TrackableMetricEventsListener stateChangeListener; private PoolStats stats; @BeforeClass public static void init() throws Exception { mockServer = RxNetty.newHttpServerBuilder(0, new RequestProcessor()).enableWireLogging(LogLevel.ERROR) .build().start(); port = mockServer.getServerPort(); } @AfterClass public static void shutdown() throws InterruptedException { mockServer.shutdown(); mockServer.waitTillShutdown(1, TimeUnit.MINUTES); } @After public void tearDown() throws Exception { if (null != client) { client.shutdown(); } } @Test public void testBasicAcquireRelease() throws Exception { client = newHttpClient(2, PoolConfig.DEFAULT_CONFIG.getMaxIdleTimeMillis(), null); HttpClientResponse<ByteBuf> response = submitAndWaitForCompletion(client, HttpClientRequest.createGet("/"), null); Assert.assertEquals("Unexpected HTTP response code.", 200, response.getStatus().code()); Assert.assertEquals("Unexpected Idle connection count.", 1, stats.getIdleCount()); Assert.assertEquals("Unexpected in use connection count.", 0, stats.getInUseCount()); Assert.assertEquals("Unexpected total connection count.", 1, stats.getTotalConnectionCount()); } @Test public void testBasicAcquireReleaseWithServerClose() throws Exception { client = newHttpClient(2, PoolConfig.DEFAULT_CONFIG.getMaxIdleTimeMillis(), null); final long[] idleCountOnComplete = { 0 }; final long[] inUseCountOnComplete = { 0 }; final long[] totalCountOnComplete = { 0 }; Action0 onComplete = new Action0() { @Override public void call() { idleCountOnComplete[0] = stats.getIdleCount(); inUseCountOnComplete[0] = stats.getInUseCount(); totalCountOnComplete[0] = stats.getTotalConnectionCount(); } }; HttpClientResponse<ByteBuf> response = submitAndWaitForCompletion(client, HttpClientRequest.createGet("test/closeConnection"), onComplete); Assert.assertEquals("Unexpected idle connection count on completion of submit. ", 0, idleCountOnComplete[0]); Assert.assertEquals("Unexpected in-use connection count on completion of submit. ", 1, inUseCountOnComplete[0]); Assert.assertEquals("Unexpected total connection count on completion of submit. ", 1, totalCountOnComplete[0]); Assert.assertEquals("Unexpected HTTP response code.", 200, response.getStatus().code()); Assert.assertEquals("Unexpected Idle connection count.", 0, stats.getIdleCount()); Assert.assertEquals("Unexpected in use connection count.", 0, stats.getInUseCount()); Assert.assertEquals("Unexpected total connection count.", 0, stats.getTotalConnectionCount()); Assert.assertEquals("Unexpected connection eviction count.", 1, stateChangeListener.getEvictionCount()); } @Test public void testReadtimeoutCloseConnection() throws Exception { HttpClient.HttpClientConfig conf = new HttpClient.HttpClientConfig.Builder() .readTimeout(1, TimeUnit.SECONDS).build(); client = newHttpClient(1, PoolConfig.DEFAULT_CONFIG.getMaxIdleTimeMillis(), conf); try { submitAndWaitForCompletion(client, HttpClientRequest.createGet("test/timeout?timeout=60000"), null); throw new AssertionError("Expected read timeout error."); } catch (ReadTimeoutException e) { waitForClose(); Assert.assertEquals("Unexpected Idle connection count.", 0, stats.getIdleCount()); Assert.assertEquals("Unexpected in use connection count.", 0, stats.getInUseCount()); Assert.assertEquals("Unexpected total connection count.", 0, stats.getTotalConnectionCount()); Assert.assertEquals("Unexpected connection eviction count.", 1, stateChangeListener.getEvictionCount()); } } @Test public void testCloseOnKeepAliveTimeout() throws Exception { client = newHttpClient(2, PoolConfig.DEFAULT_CONFIG.getMaxIdleTimeMillis(), null); HttpClientResponse<ByteBuf> response = submitAndWaitForCompletion(client, HttpClientRequest.createGet("test/keepAliveTimeout"), null); Assert.assertEquals("Unexpected HTTP response code.", 200, response.getStatus().code()); Assert.assertEquals("Unexpected Idle connection count.", 1, stats.getIdleCount()); Assert.assertEquals("Unexpected in use connection count.", 0, stats.getInUseCount()); Assert.assertEquals("Unexpected total connection count.", 1, stats.getTotalConnectionCount()); Assert.assertEquals("Unexpected reuse connection count.", 0, stateChangeListener.getReuseCount()); Thread.sleep(RequestProcessor.KEEP_ALIVE_TIMEOUT_SECONDS * 1000); // Waiting for keep-alive timeout to expire. response = submitAndWaitForCompletion(client, HttpClientRequest.createGet("/"), null); Assert.assertEquals("Unexpected HTTP response code.", 200, response.getStatus().code()); Assert.assertEquals("Unexpected Idle connection count.", 1, stats.getIdleCount()); Assert.assertEquals("Unexpected in use connection count.", 0, stats.getInUseCount()); Assert.assertEquals("Unexpected total connection count.", 1, stats.getTotalConnectionCount()); Assert.assertEquals("Unexpected reuse connection count.", 0, stateChangeListener.getReuseCount()); Assert.assertEquals("Unexpected reuse connection count.", 1, stateChangeListener.getEvictionCount()); } @Test public void testReuseWithContent() throws Exception { client = newHttpClient(1, 1000000, null); List<String> content = submitAndConsumeContent(client, HttpClientRequest.createGet("/")); Assert.assertEquals("Unexpected content fragments count.", 1, content.size()); Assert.assertEquals("Unexpected content fragment.", RequestProcessor.SINGLE_ENTITY_BODY, content.get(0)); Assert.assertEquals("Unexpected Idle connection count.", 1, stats.getIdleCount()); Assert.assertEquals("Unexpected in use connection count.", 0, stats.getInUseCount()); Assert.assertEquals("Unexpected total connection count.", 1, stats.getTotalConnectionCount()); Assert.assertEquals("Unexpected reuse connection count.", 0, stateChangeListener.getReuseCount()); content = submitAndConsumeContent(client, HttpClientRequest.createGet("/")); Assert.assertEquals("Unexpected content fragments count.", 1, content.size()); Assert.assertEquals("Unexpected content fragment.", RequestProcessor.SINGLE_ENTITY_BODY, content.get(0)); Assert.assertEquals("Unexpected Idle connection count.", 1, stats.getIdleCount()); Assert.assertEquals("Unexpected in use connection count.", 0, stats.getInUseCount()); Assert.assertEquals("Unexpected total connection count.", 1, stats.getTotalConnectionCount()); Assert.assertEquals("Unexpected reuse connection count.", 1, stateChangeListener.getReuseCount()); } @Test public void testPoolEvictions() throws InterruptedException { client = newHttpClient(1, 1000, null); final CountDownLatch latch = new CountDownLatch(1); HttpClientMetricEventsListener listener = new HttpClientMetricEventsListener() { @Override protected void onPooledConnectionEviction() { latch.countDown(); } }; client.subscribe(listener); submitAndConsumeContent(client, HttpClientRequest.createGet("/")); HttpClientRequest<ByteBuf> request = HttpClientRequest.createGet("/"); client.submit(request).subscribe(); latch.await(20, TimeUnit.SECONDS); // Wait less than default idle timeout (30 seconds) Assert.assertEquals("Pooled connection not evicted after idle timeout.", 0, latch.getCount()); } private static List<String> submitAndConsumeContent(HttpClientImpl<ByteBuf, ByteBuf> client, HttpClientRequest<ByteBuf> request) throws InterruptedException { final List<String> toReturn = new ArrayList<String>(); final CountDownLatch completionLatch = new CountDownLatch(1); Observable<HttpClientResponse<ByteBuf>> response = client.submit(request); response.flatMap(new Func1<HttpClientResponse<ByteBuf>, Observable<String>>() { @Override public Observable<String> call(HttpClientResponse<ByteBuf> response) { if (response.getStatus().code() == 200) { return response.getContent().map(new Func1<ByteBuf, String>() { @Override public String call(ByteBuf byteBuf) { return byteBuf.toString(Charset.defaultCharset()); } }); } else { return Observable .error(new AssertionError("Unexpected response code: " + response.getStatus().code())); } } }).finallyDo(new Action0() { @Override public void call() { completionLatch.countDown(); } }).toBlocking().forEach(new Action1<String>() { @Override public void call(String s) { toReturn.add(s); } }); completionLatch.await(1, TimeUnit.MINUTES); return toReturn; } private static HttpClientResponse<ByteBuf> submitAndWaitForCompletion(HttpClientImpl<ByteBuf, ByteBuf> client, HttpClientRequest<ByteBuf> request, final Action0 onComplete) throws InterruptedException { final CountDownLatch completionLatch = new CountDownLatch(1); Observable<HttpClientResponse<ByteBuf>> submit = client.submit(request); if (null != onComplete) { submit = submit.doOnCompleted(onComplete); } HttpClientResponse<ByteBuf> response = submit.finallyDo(new Action0() { @Override public void call() { completionLatch.countDown(); } }).toBlocking().last(); completionLatch.await(1, TimeUnit.MINUTES); return response; } private void waitForClose() throws InterruptedException { if (!channelCloseListener.waitForClose(1, TimeUnit.MINUTES)) { throw new AssertionError("Client channel not closed after sufficient wait."); } } private HttpClientImpl<ByteBuf, ByteBuf> newHttpClient(int maxConnections, long idleTimeout, HttpClient.HttpClientConfig clientConfig) { if (null == clientConfig) { clientConfig = HttpClient.HttpClientConfig.Builder.newDefaultConfig(); } PipelineConfigurator<HttpClientResponse<ByteBuf>, HttpClientRequest<ByteBuf>> configurator = new PipelineConfiguratorComposite<HttpClientResponse<ByteBuf>, HttpClientRequest<ByteBuf>>( PipelineConfigurators.httpClientConfigurator(), new PipelineConfigurator() { @Override public void configureNewPipeline(ChannelPipeline pipeline) { channelCloseListener.reset(); pipeline.addFirst(channelCloseListener); } }); HttpClientImpl<ByteBuf, ByteBuf> client = (HttpClientImpl<ByteBuf, ByteBuf>) new HttpClientBuilder<ByteBuf, ByteBuf>( "localhost", port).withMaxConnections(maxConnections).withIdleConnectionsTimeoutMillis(idleTimeout) .config(clientConfig).enableWireLogging(LogLevel.DEBUG).pipelineConfigurator(configurator) .build(); stateChangeListener = new TrackableMetricEventsListener(); client.subscribe(stateChangeListener); stats = new PoolStats(); client.subscribe(stats); return client; } }