/* Copyright (c) 2012 LinkedIn Corp. 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. */ /** * $Id: $ */ package com.linkedin.r2.transport.http.client; import com.linkedin.common.callback.Callback; import com.linkedin.common.callback.FutureCallback; import com.linkedin.common.util.None; import com.linkedin.r2.RemoteInvocationException; import com.linkedin.r2.filter.R2Constants; import com.linkedin.r2.message.RequestContext; import com.linkedin.r2.message.rest.RestRequest; import com.linkedin.r2.message.rest.RestRequestBuilder; import com.linkedin.r2.message.rest.RestResponse; import com.linkedin.r2.transport.common.bridge.client.TransportCallbackAdapter; import com.linkedin.r2.transport.common.bridge.common.TransportCallback; import com.linkedin.r2.transport.http.common.HttpProtocolVersion; import io.netty.channel.Channel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.EncoderException; import io.netty.handler.codec.TooLongFrameException; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import java.io.IOException; import java.net.SocketAddress; import java.net.URI; import java.net.UnknownHostException; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * @author Steven Ihde * @author Ang Xu * @version $Revision: $ */ public class TestHttpNettyClient { private NioEventLoopGroup _eventLoop; private ScheduledExecutorService _scheduler; private static final int TEST_MAX_RESPONSE_SIZE = 500000; private static final int TEST_MAX_HEADER_SIZE = 50000; private static final int RESPONSE_OK = 1; private static final int TOO_LARGE = 2; @BeforeClass public void setup() { _eventLoop = new NioEventLoopGroup(); _scheduler = Executors.newSingleThreadScheduledExecutor(); } @AfterClass public void tearDown() { _scheduler.shutdown(); _eventLoop.shutdownGracefully(); } @Test public void testNoChannelTimeout() throws InterruptedException { HttpNettyClient client = new HttpNettyClient(new NoCreations(_scheduler), _scheduler, 500, 500, 1024 * 1024 * 2); RestRequest r = new RestRequestBuilder(URI.create("http://localhost/")).build(); FutureCallback<RestResponse> cb = new FutureCallback<RestResponse>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<RestResponse>(cb); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), callback); try { // This timeout needs to be significantly larger than the getTimeout of the netty client; // we're testing that the client will generate its own timeout cb.get(30, TimeUnit.SECONDS); Assert.fail("Get was supposed to time out"); } catch (TimeoutException e) { // TimeoutException means the timeout for Future.get() elapsed and nothing happened. // Instead, we are expecting our callback to be invoked before the future timeout // with a timeout generated by the HttpNettyClient. Assert.fail("Unexpected TimeoutException, should have been ExecutionException", e); } catch (ExecutionException e) { verifyCauseChain(e, RemoteInvocationException.class, TimeoutException.class); } } @Test public void testNoResponseTimeout() throws InterruptedException, IOException { TestServer testServer = new TestServer(); HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler).setRequestTimeout(500).setIdleTimeout(10000) .setShutdownTimeout(500).buildRest(); RestRequest r = new RestRequestBuilder(testServer.getNoResponseURI()).build(); FutureCallback<RestResponse> cb = new FutureCallback<RestResponse>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<RestResponse>(cb); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), callback); try { // This timeout needs to be significantly larger than the getTimeout of the netty client; // we're testing that the client will generate its own timeout cb.get(30, TimeUnit.SECONDS); Assert.fail("Get was supposed to time out"); } catch (TimeoutException e) { // TimeoutException means the timeout for Future.get() elapsed and nothing happened. // Instead, we are expecting our callback to be invoked before the future timeout // with a timeout generated by the HttpNettyClient. Assert.fail("Unexpected TimeoutException, should have been ExecutionException", e); } catch (ExecutionException e) { verifyCauseChain(e, RemoteInvocationException.class, TimeoutException.class); } testServer.shutdown(); } @Test public void testBadAddress() throws InterruptedException, IOException, TimeoutException { HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler) .setRequestTimeout(30000) .setIdleTimeout(10000) .setShutdownTimeout(500) .buildRest(); RestRequest r = new RestRequestBuilder(URI.create("http://this.host.does.not.exist.linkedin.com")).build(); FutureCallback<RestResponse> cb = new FutureCallback<RestResponse>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<RestResponse>(cb); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), callback); try { cb.get(30, TimeUnit.SECONDS); Assert.fail("Get was supposed to fail"); } catch (ExecutionException e) { verifyCauseChain(e, RemoteInvocationException.class, UnknownHostException.class); } } @Test public void testRequestContextAttributes() throws InterruptedException, IOException, TimeoutException { HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler).buildRest(); RestRequest r = new RestRequestBuilder(URI.create("http://localhost")).build(); FutureCallback<RestResponse> cb = new FutureCallback<>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<>(cb); RequestContext requestContext = new RequestContext(); client.restRequest(r, requestContext, new HashMap<>(), callback); final String actualRemoteAddress = (String) requestContext.getLocalAttr(R2Constants.REMOTE_SERVER_ADDR); final HttpProtocolVersion actualProtocolVersion = (HttpProtocolVersion) requestContext.getLocalAttr(R2Constants.HTTP_PROTOCOL_VERSION); Assert.assertTrue("127.0.0.1".equals(actualRemoteAddress) || "0:0:0:0:0:0:0:1".equals(actualRemoteAddress), "Actual remote client address is not expected. " + "The local attribute field must be IP address in string type"); Assert.assertEquals(actualProtocolVersion, HttpProtocolVersion.HTTP_1_1); } @Test public void testMaxResponseSize() throws InterruptedException, IOException, TimeoutException { testResponseSize(TEST_MAX_RESPONSE_SIZE - 1, RESPONSE_OK); testResponseSize(TEST_MAX_RESPONSE_SIZE, RESPONSE_OK); testResponseSize(TEST_MAX_RESPONSE_SIZE + 1, TOO_LARGE); } public void testResponseSize(int responseSize, int expectedResult) throws InterruptedException, IOException, TimeoutException { TestServer testServer = new TestServer(); HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler).setRequestTimeout(50000).setIdleTimeout(10000) .setShutdownTimeout(500).setMaxResponseSize(TEST_MAX_RESPONSE_SIZE).buildRest(); RestRequest r = new RestRequestBuilder(testServer.getResponseOfSizeURI(responseSize)).build(); FutureCallback<RestResponse> cb = new FutureCallback<RestResponse>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<RestResponse>(cb); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), callback); try { cb.get(30, TimeUnit.SECONDS); if (expectedResult == TOO_LARGE) { Assert.fail("Max response size exceeded, expected exception. "); } } catch (ExecutionException e) { if (expectedResult == RESPONSE_OK) { Assert.fail("Unexpected ExecutionException, response was <= max response size."); } verifyCauseChain(e, RemoteInvocationException.class, TooLongFrameException.class); } testServer.shutdown(); } @Test public void testMaxHeaderSize() throws InterruptedException, IOException, TimeoutException { testHeaderSize(TEST_MAX_HEADER_SIZE - 1, RESPONSE_OK); testHeaderSize(TEST_MAX_HEADER_SIZE, RESPONSE_OK); testHeaderSize(TEST_MAX_HEADER_SIZE + 1, TOO_LARGE); } public void testHeaderSize(int headerSize, int expectedResult) throws InterruptedException, IOException, TimeoutException { TestServer testServer = new TestServer(); HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler).setRequestTimeout(5000000).setIdleTimeout(10000) .setShutdownTimeout(500).setMaxHeaderSize(TEST_MAX_HEADER_SIZE).buildRest(); RestRequest r = new RestRequestBuilder(testServer.getResponseWithHeaderSizeURI(headerSize)).build(); FutureCallback<RestResponse> cb = new FutureCallback<RestResponse>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<RestResponse>(cb); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), callback); try { RestResponse response = cb.get(300, TimeUnit.SECONDS); if (expectedResult == TOO_LARGE) { Assert.fail("Max header size exceeded, expected exception. "); } } catch (ExecutionException e) { if (expectedResult == RESPONSE_OK) { Assert.fail("Unexpected ExecutionException, header was <= max header size."); } verifyCauseChain(e, RemoteInvocationException.class, TooLongFrameException.class); } testServer.shutdown(); } @Test public void testReceiveBadHeader() throws InterruptedException, IOException { TestServer testServer = new TestServer(); HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler) .setRequestTimeout(10000) .setIdleTimeout(10000) .setShutdownTimeout(500).buildRest(); RestRequest r = new RestRequestBuilder(testServer.getBadHeaderURI()).build(); FutureCallback<RestResponse> cb = new FutureCallback<RestResponse>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<RestResponse>(cb); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), callback); try { cb.get(30, TimeUnit.SECONDS); Assert.fail("Get was supposed to fail"); } catch (TimeoutException e) { Assert.fail("Unexpected TimeoutException, should have been ExecutionException", e); } catch (ExecutionException e) { verifyCauseChain(e, RemoteInvocationException.class, IllegalArgumentException.class); } testServer.shutdown(); } @Test public void testSendBadHeader() throws Exception { TestServer testServer = new TestServer(); HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler) .setRequestTimeout(10000) .setIdleTimeout(10000) .setShutdownTimeout(500).buildRest(); RestRequestBuilder rb = new RestRequestBuilder(testServer.getRequestURI()); rb.setHeader("x", "makenettyunhappy\u000Bblah"); RestRequest request = rb.build(); FutureCallback<RestResponse> cb = new FutureCallback<RestResponse>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<RestResponse>(cb); client.restRequest(request, new RequestContext(), new HashMap<String, String>(), callback); try { cb.get(30, TimeUnit.SECONDS); Assert.fail("Should fail sending request"); } catch (TimeoutException ex) { Assert.fail("Unexpected TimeoutException, should have been ExecutionException", ex); } catch (ExecutionException ex) { verifyCauseChain(ex, RemoteInvocationException.class, EncoderException.class, IllegalArgumentException.class); } testServer.shutdown(); } @Test public void testShutdown() throws ExecutionException, TimeoutException, InterruptedException { HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler) .setRequestTimeout(500) .setIdleTimeout(10000) .setShutdownTimeout(500) .buildRest(); FutureCallback<None> shutdownCallback = new FutureCallback<None>(); client.shutdown(shutdownCallback); shutdownCallback.get(30, TimeUnit.SECONDS); // Now verify a new request will also fail RestRequest r = new RestRequestBuilder(URI.create("http://no.such.host.linkedin.com")).build(); FutureCallback<RestResponse> callback = new FutureCallback<RestResponse>(); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), new TransportCallbackAdapter<RestResponse>(callback)); try { callback.get(30, TimeUnit.SECONDS); } catch (ExecutionException e) { // Expected } } @Test public void testShutdownStuckInPool() throws InterruptedException, ExecutionException, TimeoutException { // Test that shutdown works when the outstanding request is stuck in the pool waiting for a channel HttpNettyClient client = new HttpNettyClient(new NoCreations(_scheduler), _scheduler, 60000, 1, 1024 * 1024 * 2); RestRequest r = new RestRequestBuilder(URI.create("http://some.host/")).build(); FutureCallback<RestResponse> futureCallback = new FutureCallback<RestResponse>(); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), new TransportCallbackAdapter<RestResponse>(futureCallback)); FutureCallback<None> shutdownCallback = new FutureCallback<None>(); client.shutdown(shutdownCallback); shutdownCallback.get(30, TimeUnit.SECONDS); try { futureCallback.get(30, TimeUnit.SECONDS); Assert.fail("get should have thrown exception"); } catch (ExecutionException e) { verifyCauseChain(e, RemoteInvocationException.class, TimeoutException.class); } } @Test public void testShutdownRequestOutstanding() throws IOException, ExecutionException, TimeoutException, InterruptedException { // Test that it works when the shutdown kills the outstanding request... testShutdownRequestOutstanding(500, 60000, RemoteInvocationException.class, TimeoutException.class); } @Test public void testShutdownRequestOutstanding2() throws IOException, ExecutionException, TimeoutException, InterruptedException { // Test that it works when the request timeout kills the outstanding request... testShutdownRequestOutstanding(60000, 500, RemoteInvocationException.class, // sometimes the test fails with ChannelClosedException // TimeoutException.class Exception.class); } private void testShutdownRequestOutstanding(int shutdownTimeout, int requestTimeout, Class<?>... causeChain) throws InterruptedException, IOException, ExecutionException, TimeoutException { TestServer testServer = new TestServer(); HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler).setRequestTimeout(requestTimeout) .setShutdownTimeout(shutdownTimeout).buildRest(); RestRequest r = new RestRequestBuilder(testServer.getNoResponseURI()).build(); FutureCallback<RestResponse> cb = new FutureCallback<RestResponse>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<RestResponse>(cb); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), callback); FutureCallback<None> shutdownCallback = new FutureCallback<None>(); client.shutdown(shutdownCallback); shutdownCallback.get(30, TimeUnit.SECONDS); try { // This timeout needs to be significantly larger than the getTimeout of the netty client; // we're testing that the client will generate its own timeout cb.get(30, TimeUnit.SECONDS); Assert.fail("Get was supposed to time out"); } catch (TimeoutException e) { // TimeoutException means the timeout for Future.get() elapsed and nothing happened. // Instead, we are expecting our callback to be invoked before the future timeout // with a timeout generated by the HttpNettyClient. Assert.fail("Get timed out, should have thrown ExecutionException", e); } catch (ExecutionException e) { verifyCauseChain(e, causeChain); } testServer.shutdown(); } private static void verifyCauseChain(Throwable throwable, Class<?>... causes) { Throwable t = throwable; for (Class<?> c : causes) { Throwable cause = t.getCause(); if (cause == null) { Assert.fail("Cause chain ended too early", throwable); } if (!c.isAssignableFrom(cause.getClass())) { Assert.fail("Expected cause " + c.getName() + " not " + cause.getClass().getName(), throwable); } t = cause; } } // Test that cannot pass pass SSLParameters without SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory1() throws NoSuchAlgorithmException { try { new HttpClientBuilder(_eventLoop, _scheduler) .setSSLParameters(new SSLParameters()) .buildRest(); } catch (IllegalArgumentException e) { // Check exception message to make sure it's the expected one. Assert.assertEquals(e.getMessage(), "SSLParameters passed with no SSLContext"); } } // Test that cannot set cipher suites in SSLParameters that don't have any match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory2Fail() throws NoSuchAlgorithmException { String[] requestedCipherSuites = {"Unsupported"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setCipherSuites(requestedCipherSuites); try { new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildRest(); } catch (IllegalArgumentException e) { // Check exception message to make sure it's the expected one. Assert.assertEquals(e.getMessage(), "None of the requested cipher suites: [Unsupported] are found in SSLContext"); } } // Test that can set cipher suites in SSLParameters that have at least one match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory2Pass() throws NoSuchAlgorithmException { String[] requestedCipherSuites = {"Unsupported", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setCipherSuites(requestedCipherSuites); new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildRest(); } // Test that cannot set protocols in SSLParameters that don't have any match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory3Fail() throws NoSuchAlgorithmException { String[] requestedProtocols = {"Unsupported"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setProtocols(requestedProtocols); try { new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildRest(); } catch (IllegalArgumentException e) { // Check exception message to make sure it's the expected one. Assert.assertEquals(e.getMessage(), "None of the requested protocols: [Unsupported] are found in SSLContext"); } } // Test that can set protocols in SSLParameters that have at least one match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory3Pass() throws NoSuchAlgorithmException { String[] requestedProtocols = {"Unsupported", "TLSv1"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setProtocols(requestedProtocols); new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildRest(); } @Test public void testPoolStatsProviderManager() throws InterruptedException, ExecutionException, TimeoutException { final CountDownLatch setLatch = new CountDownLatch(1); final CountDownLatch removeLatch = new CountDownLatch(1); AbstractJmxManager manager = new AbstractJmxManager() { @Override public void onProviderCreate(PoolStatsProvider provider) { setLatch.countDown(); } @Override public void onProviderShutdown(PoolStatsProvider provider) { removeLatch.countDown(); } }; HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler) .setJmxManager(manager) .buildRest(); // test setPoolStatsProvider try { setLatch.await(30, TimeUnit.SECONDS); } catch (InterruptedException e) { Assert.fail("PoolStatsAware setPoolStatsProvider didn't get called when creating channel pool."); } // test removePoolStatsProvider FutureCallback<None> shutdownCallback = new FutureCallback<None>(); client.shutdown(shutdownCallback); try { removeLatch.await(30, TimeUnit.SECONDS); } catch (InterruptedException e) { Assert.fail("PoolStatsAware removePoolStatsProvider didn't get called when shutting down channel pool."); } shutdownCallback.get(30, TimeUnit.SECONDS); } @Test (enabled = false) public void testMakingOutboundHttpsRequest() throws NoSuchAlgorithmException, InterruptedException, ExecutionException, TimeoutException { SSLContext context = SSLContext.getDefault(); SSLParameters sslParameters = context.getDefaultSSLParameters(); HttpNettyClient client = new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(context) .setSSLParameters(sslParameters) .buildRest(); RestRequest r = new RestRequestBuilder(URI.create("https://www.howsmyssl.com/a/check")).build(); FutureCallback<RestResponse> cb = new FutureCallback<RestResponse>(); TransportCallback<RestResponse> callback = new TransportCallbackAdapter<RestResponse>(cb); client.restRequest(r, new RequestContext(), new HashMap<String, String>(), callback); cb.get(30, TimeUnit.SECONDS); } @Test public void testFailBackoff() throws Exception { final int WARM_UP = 10; final int N = 5; final int MAX_RATE_LIMITING_PERIOD = 500; final CountDownLatch warmUpLatch = new CountDownLatch(WARM_UP); final CountDownLatch latch = new CountDownLatch(N); final AtomicReference<Boolean> isShutdown = new AtomicReference<>(false); AsyncPool<Channel> testPool = new AsyncPoolImpl<>("test pool", new AsyncPool.Lifecycle<Channel>() { @Override public void create(Callback<Channel> callback) { if (warmUpLatch.getCount() > 0) { warmUpLatch.countDown(); } else { latch.countDown(); } callback.onError(new Throwable("Oops...")); } @Override public boolean validateGet(Channel obj) { return false; } @Override public boolean validatePut(Channel obj) { return false; } @Override public void destroy(Channel obj, boolean error, Callback<Channel> callback) { } @Override public PoolStats.LifecycleStats getStats() { return null; } }, 200, 30000, _scheduler, Integer.MAX_VALUE, AsyncPoolImpl.Strategy.MRU, 0, new ExponentialBackOffRateLimiter(0, MAX_RATE_LIMITING_PERIOD, Math.max(10, MAX_RATE_LIMITING_PERIOD / 32), _scheduler) ); HttpNettyClient client = new HttpNettyClient(address -> testPool, _scheduler, 500, 500, 1024 * 1024 * 2); final RestRequest r = new RestRequestBuilder(URI.create("http://localhost:8080/")).setMethod("GET").build(); final ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { while (!isShutdown.get()) { try { FutureCallback<RestResponse> callback = new FutureCallback<>(); client.restRequest(r, new RequestContext(), new HashMap<>(), new TransportCallbackAdapter<RestResponse>(callback)); callback.get(); } catch (Exception e) { // ignore } } }); // First ensure a bunch fail to get the rate limiting going warmUpLatch.await(120, TimeUnit.SECONDS); // Now we should be rate limited long start = System.currentTimeMillis(); System.err.println("Starting at " + start); long lowTolerance = N * MAX_RATE_LIMITING_PERIOD * 4 / 5; long highTolerance = N * MAX_RATE_LIMITING_PERIOD * 5 / 4; Assert.assertTrue(latch.await(highTolerance, TimeUnit.MILLISECONDS), "Should have finished within " + highTolerance + "ms"); long elapsed = System.currentTimeMillis() - start; Assert.assertTrue(elapsed > lowTolerance, "Should have finished after " + lowTolerance + "ms (took " + elapsed +")"); // shutdown everything isShutdown.set(true); executor.shutdown(); } private static class NoCreations implements ChannelPoolFactory { private final ScheduledExecutorService _scheduler; public NoCreations(ScheduledExecutorService scheduler) { _scheduler = scheduler; } @Override public AsyncPool<Channel> getPool(SocketAddress address) { return new AsyncPoolImpl<Channel>("fake pool", new AsyncPool.Lifecycle<Channel>() { @Override public void create(Callback<Channel> channelCallback) { } @Override public boolean validateGet(Channel obj) { return false; } @Override public boolean validatePut(Channel obj) { return false; } @Override public void destroy(Channel obj, boolean error, Callback<Channel> channelCallback) { } @Override public PoolStats.LifecycleStats getStats() { return null; } }, 0, 0, _scheduler); } } }