blob: a2a2653cd266c603f8e36cec23d95694ef06cb73 [file] [log] [blame]
/*
* 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());
}
}