/* * Copyright (C) 2013 Square, 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 com.squareup.okhttp; import com.squareup.okhttp.internal.RecordingHostnameVerifier; import com.squareup.okhttp.internal.SslContextBuilder; import com.squareup.okhttp.internal.Util; import com.squareup.okhttp.internal.http.HttpAuthenticator; import com.squareup.okhttp.mockwebserver.MockWebServer; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.util.Arrays; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public final class ConnectionPoolTest { private static final int KEEP_ALIVE_DURATION_MS = 5000; private static final SSLContext sslContext = SslContextBuilder.localhost(); private MockWebServer spdyServer; private InetSocketAddress spdySocketAddress; private Address spdyAddress; private MockWebServer httpServer; private Address httpAddress; private InetSocketAddress httpSocketAddress; private ConnectionPool pool; private Connection httpA; private Connection httpB; private Connection httpC; private Connection httpD; private Connection httpE; private Connection spdyA; private Object owner; @Before public void setUp() throws Exception { setUp(2); } private void setUp(int poolSize) throws Exception { SocketFactory socketFactory = SocketFactory.getDefault(); spdyServer = new MockWebServer(); httpServer = new MockWebServer(); spdyServer.useHttps(sslContext.getSocketFactory(), false); httpServer.play(); httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), socketFactory, null, null, HttpAuthenticator.SYSTEM_DEFAULT, null, Protocol.SPDY3_AND_HTTP11); httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()), httpServer.getPort()); spdyServer.play(); spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), socketFactory, sslContext.getSocketFactory(), new RecordingHostnameVerifier(), HttpAuthenticator.SYSTEM_DEFAULT, null,Protocol.SPDY3_AND_HTTP11); spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()), spdyServer.getPort()); Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true); Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true); pool = new ConnectionPool(poolSize, KEEP_ALIVE_DURATION_MS); httpA = new Connection(pool, httpRoute); httpA.connect(200, 200, null); httpB = new Connection(pool, httpRoute); httpB.connect(200, 200, null); httpC = new Connection(pool, httpRoute); httpC.connect(200, 200, null); httpD = new Connection(pool, httpRoute); httpD.connect(200, 200, null); httpE = new Connection(pool, httpRoute); httpE.connect(200, 200, null); spdyA = new Connection(pool, spdyRoute); spdyA.connect(20000, 20000, null); owner = new Object(); httpA.setOwner(owner); httpB.setOwner(owner); httpC.setOwner(owner); httpD.setOwner(owner); httpE.setOwner(owner); } @After public void tearDown() throws Exception { httpServer.shutdown(); spdyServer.shutdown(); Util.closeQuietly(httpA); Util.closeQuietly(httpB); Util.closeQuietly(httpC); Util.closeQuietly(httpD); Util.closeQuietly(httpE); Util.closeQuietly(spdyA); } private void resetWithPoolSize(int poolSize) throws Exception { tearDown(); setUp(poolSize); } @Test public void poolSingleHttpConnection() throws Exception { resetWithPoolSize(1); Connection connection = pool.get(httpAddress); assertNull(connection); connection = new Connection( pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true)); connection.connect(200, 200, null); connection.setOwner(owner); assertEquals(0, pool.getConnectionCount()); pool.recycle(connection); assertNull(connection.getOwner()); assertEquals(1, pool.getConnectionCount()); assertEquals(1, pool.getHttpConnectionCount()); assertEquals(0, pool.getSpdyConnectionCount()); Connection recycledConnection = pool.get(httpAddress); assertNull(connection.getOwner()); assertEquals(connection, recycledConnection); assertTrue(recycledConnection.isAlive()); recycledConnection = pool.get(httpAddress); assertNull(recycledConnection); } @Test public void poolPrefersMostRecentlyRecycled() throws Exception { pool.recycle(httpA); pool.recycle(httpB); pool.recycle(httpC); assertPooled(pool, httpC, httpB); } @Test public void getSpdyConnection() throws Exception { pool.share(spdyA); assertSame(spdyA, pool.get(spdyAddress)); assertPooled(pool, spdyA); } @Test public void getHttpConnection() throws Exception { pool.recycle(httpA); assertSame(httpA, pool.get(httpAddress)); assertPooled(pool); } @Test public void idleConnectionNotReturned() throws Exception { pool.recycle(httpA); Thread.sleep(KEEP_ALIVE_DURATION_MS * 2); assertNull(pool.get(httpAddress)); assertPooled(pool); } @Test public void maxIdleConnectionLimitIsEnforced() throws Exception { pool.recycle(httpA); pool.recycle(httpB); pool.recycle(httpC); pool.recycle(httpD); assertPooled(pool, httpD, httpC); } @Test public void expiredConnectionsAreEvicted() throws Exception { pool.recycle(httpA); pool.recycle(httpB); Thread.sleep(2 * KEEP_ALIVE_DURATION_MS); pool.get(spdyAddress); // Force the cleanup callable to run. assertPooled(pool); } @Test public void nonAliveConnectionNotReturned() throws Exception { pool.recycle(httpA); httpA.close(); assertNull(pool.get(httpAddress)); assertPooled(pool); } @Test public void differentAddressConnectionNotReturned() throws Exception { pool.recycle(httpA); assertNull(pool.get(spdyAddress)); assertPooled(pool, httpA); } @Test public void gettingSpdyConnectionPromotesItToFrontOfQueue() throws Exception { pool.share(spdyA); pool.recycle(httpA); assertPooled(pool, httpA, spdyA); assertSame(spdyA, pool.get(spdyAddress)); assertPooled(pool, spdyA, httpA); } @Test public void gettingConnectionReturnsOldestFirst() throws Exception { pool.recycle(httpA); pool.recycle(httpB); assertSame(httpA, pool.get(httpAddress)); } @Test public void recyclingNonAliveConnectionClosesThatConnection() throws Exception { httpA.getSocket().shutdownInput(); pool.recycle(httpA); // Should close httpA. assertTrue(httpA.getSocket().isClosed()); } @Test public void shareHttpConnectionFails() throws Exception { try { pool.share(httpA); fail(); } catch (IllegalArgumentException expected) { } assertPooled(pool); } @Test public void recycleSpdyConnectionDoesNothing() throws Exception { pool.recycle(spdyA); assertPooled(pool); } @Test public void validateIdleSpdyConnectionTimeout() throws Exception { pool.share(spdyA); Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7)); assertNull(pool.get(httpAddress)); assertPooled(pool, spdyA); // Connection should still be in the pool. Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4)); assertNull(pool.get(httpAddress)); assertPooled(pool); } @Test public void validateIdleHttpConnectionTimeout() throws Exception { pool.recycle(httpA); Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7)); assertNull(pool.get(spdyAddress)); assertPooled(pool, httpA); // Connection should still be in the pool. Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4)); assertNull(pool.get(spdyAddress)); assertPooled(pool); } @Test public void maxConnections() throws IOException, InterruptedException { // Pool should be empty. assertEquals(0, pool.getConnectionCount()); // http A should be added to the pool. pool.recycle(httpA); assertEquals(1, pool.getConnectionCount()); assertEquals(1, pool.getHttpConnectionCount()); assertEquals(0, pool.getSpdyConnectionCount()); // http B should be added to the pool. pool.recycle(httpB); assertEquals(2, pool.getConnectionCount()); assertEquals(2, pool.getHttpConnectionCount()); assertEquals(0, pool.getSpdyConnectionCount()); // http C should be added and http A should be removed. pool.recycle(httpC); Thread.sleep(50); assertEquals(2, pool.getConnectionCount()); assertEquals(2, pool.getHttpConnectionCount()); assertEquals(0, pool.getSpdyConnectionCount()); // spdy A should be added and http B should be removed. pool.share(spdyA); Thread.sleep(50); assertEquals(2, pool.getConnectionCount()); assertEquals(1, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // http C should be removed from the pool. Connection recycledHttpConnection = pool.get(httpAddress); recycledHttpConnection.setOwner(owner); assertNotNull(recycledHttpConnection); assertTrue(recycledHttpConnection.isAlive()); assertEquals(1, pool.getConnectionCount()); assertEquals(0, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // spdy A will be returned and kept in the pool. Connection sharedSpdyConnection = pool.get(spdyAddress); assertNotNull(sharedSpdyConnection); assertEquals(spdyA, sharedSpdyConnection); assertEquals(1, pool.getConnectionCount()); assertEquals(0, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // Nothing should change. pool.recycle(httpC); Thread.sleep(50); assertEquals(2, pool.getConnectionCount()); assertEquals(1, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // An http connection should be removed from the pool. recycledHttpConnection = pool.get(httpAddress); assertNotNull(recycledHttpConnection); assertTrue(recycledHttpConnection.isAlive()); assertEquals(1, pool.getConnectionCount()); assertEquals(0, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // spdy A will be returned and kept in the pool. Pool shouldn't change. sharedSpdyConnection = pool.get(spdyAddress); assertEquals(spdyA, sharedSpdyConnection); assertNotNull(sharedSpdyConnection); assertEquals(1, pool.getConnectionCount()); assertEquals(0, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // http D should be added to the pool. pool.recycle(httpD); Thread.sleep(50); assertEquals(2, pool.getConnectionCount()); assertEquals(1, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // http E should be added to the pool. spdy A should be removed from the pool. pool.recycle(httpE); Thread.sleep(50); assertEquals(2, pool.getConnectionCount()); assertEquals(2, pool.getHttpConnectionCount()); assertEquals(0, pool.getSpdyConnectionCount()); } @Test public void connectionCleanup() throws IOException, InterruptedException { ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS); // Add 3 connections to the pool. pool.recycle(httpA); pool.recycle(httpB); pool.share(spdyA); assertEquals(3, pool.getConnectionCount()); assertEquals(2, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // Kill http A. Util.closeQuietly(httpA); // Force pool to run a clean up. assertNotNull(pool.get(spdyAddress)); Thread.sleep(50); assertEquals(2, pool.getConnectionCount()); assertEquals(1, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); Thread.sleep(KEEP_ALIVE_DURATION_MS); // Force pool to run a clean up. assertNull(pool.get(httpAddress)); assertNull(pool.get(spdyAddress)); Thread.sleep(50); assertEquals(0, pool.getConnectionCount()); assertEquals(0, pool.getHttpConnectionCount()); assertEquals(0, pool.getSpdyConnectionCount()); } // Tests to demonstrate Android bug http://b/18369687 and the solution to it. @Test public void connectionCleanup_draining() throws IOException, InterruptedException { ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS); // Add 3 connections to the pool. pool.recycle(httpA); pool.recycle(httpB); pool.share(spdyA); assertEquals(3, pool.getConnectionCount()); assertEquals(2, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // With no method calls made to the pool it will not clean up any connections. Thread.sleep(KEEP_ALIVE_DURATION_MS * 5); assertEquals(3, pool.getConnectionCount()); assertEquals(2, pool.getHttpConnectionCount()); assertEquals(1, pool.getSpdyConnectionCount()); // Change the pool into a mode that will clean up connections. pool.enterDrainMode(); // Give the drain thread a chance to run. for (int i = 0; i < 5; i++) { Thread.sleep(KEEP_ALIVE_DURATION_MS); if (pool.isDrained()) { break; } } // All connections should have drained. assertEquals(0, pool.getConnectionCount()); } @Test public void evictAllConnections() throws Exception { resetWithPoolSize(10); pool.recycle(httpA); Util.closeQuietly(httpA); // Include a closed connection in the pool. pool.recycle(httpB); pool.share(spdyA); int connectionCount = pool.getConnectionCount(); assertTrue(connectionCount == 2 || connectionCount == 3); pool.evictAll(); assertEquals(0, pool.getConnectionCount()); } @Test public void closeIfOwnedBy() throws Exception { httpA.closeIfOwnedBy(owner); assertFalse(httpA.isAlive()); assertFalse(httpA.clearOwner()); } @Test public void closeIfOwnedByDoesNothingIfNotOwner() throws Exception { httpA.closeIfOwnedBy(new Object()); assertTrue(httpA.isAlive()); assertTrue(httpA.clearOwner()); } @Test public void closeIfOwnedByFailsForSpdyConnections() throws Exception { try { spdyA.closeIfOwnedBy(owner); fail(); } catch (IllegalStateException expected) { } } private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception { assertEquals(Arrays.asList(connections), pool.getConnections()); } }