ConnectionPoolTest.java revision 7aeaaefc891f6221f4b2cce536b1c1e816e09794
1/* 2 * Copyright (C) 2013 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.squareup.okhttp; 17 18import com.squareup.okhttp.internal.Internal; 19import com.squareup.okhttp.internal.RecordingHostnameVerifier; 20import com.squareup.okhttp.internal.SslContextBuilder; 21import com.squareup.okhttp.internal.Util; 22import com.squareup.okhttp.internal.http.AuthenticatorAdapter; 23import com.squareup.okhttp.internal.http.RecordingProxySelector; 24import com.squareup.okhttp.mockwebserver.MockWebServer; 25import java.io.IOException; 26import java.net.InetAddress; 27import java.net.InetSocketAddress; 28import java.net.Proxy; 29import java.util.Arrays; 30import java.util.List; 31import java.util.concurrent.Executor; 32import javax.net.SocketFactory; 33import javax.net.ssl.SSLContext; 34import org.junit.After; 35import org.junit.Before; 36import org.junit.Test; 37 38import static org.junit.Assert.assertEquals; 39import static org.junit.Assert.assertFalse; 40import static org.junit.Assert.assertNotNull; 41import static org.junit.Assert.assertNull; 42import static org.junit.Assert.assertSame; 43import static org.junit.Assert.assertTrue; 44import static org.junit.Assert.fail; 45 46public final class ConnectionPoolTest { 47 static { 48 Internal.initializeInstanceForTests(); 49 } 50 51 private static final List<ConnectionSpec> CONNECTION_SPECS = Util.immutableList( 52 ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT); 53 54 private static final int KEEP_ALIVE_DURATION_MS = 5000; 55 private static final SSLContext sslContext = SslContextBuilder.localhost(); 56 57 private MockWebServer spdyServer; 58 private InetSocketAddress spdySocketAddress; 59 private Address spdyAddress; 60 61 private MockWebServer httpServer; 62 private Address httpAddress; 63 private InetSocketAddress httpSocketAddress; 64 65 private ConnectionPool pool; 66 private FakeExecutor cleanupExecutor; 67 private Connection httpA; 68 private Connection httpB; 69 private Connection httpC; 70 private Connection httpD; 71 private Connection httpE; 72 private Connection spdyA; 73 74 private Object owner; 75 76 @Before public void setUp() throws Exception { 77 setUp(2); 78 } 79 80 private void setUp(int poolSize) throws Exception { 81 SocketFactory socketFactory = SocketFactory.getDefault(); 82 RecordingProxySelector proxySelector = new RecordingProxySelector(); 83 84 spdyServer = new MockWebServer(); 85 httpServer = new MockWebServer(); 86 spdyServer.useHttps(sslContext.getSocketFactory(), false); 87 88 httpServer.start(); 89 httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), socketFactory, null, 90 null, null, AuthenticatorAdapter.INSTANCE, null, 91 Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1), CONNECTION_SPECS, proxySelector); 92 httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()), 93 httpServer.getPort()); 94 95 spdyServer.start(); 96 spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), socketFactory, 97 sslContext.getSocketFactory(), new RecordingHostnameVerifier(), CertificatePinner.DEFAULT, 98 AuthenticatorAdapter.INSTANCE, null, Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1), 99 CONNECTION_SPECS, proxySelector); 100 spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()), 101 spdyServer.getPort()); 102 103 Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress); 104 Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress); 105 pool = new ConnectionPool(poolSize, KEEP_ALIVE_DURATION_MS); 106 // Disable the automatic execution of the cleanup. 107 cleanupExecutor = new FakeExecutor(); 108 pool.replaceCleanupExecutorForTests(cleanupExecutor); 109 httpA = new Connection(pool, httpRoute); 110 httpA.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */); 111 httpB = new Connection(pool, httpRoute); 112 httpB.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */); 113 httpC = new Connection(pool, httpRoute); 114 httpC.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */); 115 httpD = new Connection(pool, httpRoute); 116 httpD.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */); 117 httpE = new Connection(pool, httpRoute); 118 httpE.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */); 119 spdyA = new Connection(pool, spdyRoute); 120 spdyA.connect(20000, 20000, 2000, null, CONNECTION_SPECS, false /* connectionRetryEnabled */); 121 122 owner = new Object(); 123 httpA.setOwner(owner); 124 httpB.setOwner(owner); 125 httpC.setOwner(owner); 126 httpD.setOwner(owner); 127 httpE.setOwner(owner); 128 } 129 130 @After public void tearDown() throws Exception { 131 httpServer.shutdown(); 132 spdyServer.shutdown(); 133 134 Util.closeQuietly(httpA.getSocket()); 135 Util.closeQuietly(httpB.getSocket()); 136 Util.closeQuietly(httpC.getSocket()); 137 Util.closeQuietly(httpD.getSocket()); 138 Util.closeQuietly(httpE.getSocket()); 139 Util.closeQuietly(spdyA.getSocket()); 140 } 141 142 private void resetWithPoolSize(int poolSize) throws Exception { 143 tearDown(); 144 setUp(poolSize); 145 } 146 147 @Test public void poolSingleHttpConnection() throws Exception { 148 resetWithPoolSize(1); 149 Connection connection = pool.get(httpAddress); 150 assertNull(connection); 151 152 connection = new Connection(pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress)); 153 connection.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */); 154 connection.setOwner(owner); 155 assertEquals(0, pool.getConnectionCount()); 156 157 pool.recycle(connection); 158 assertNull(connection.getOwner()); 159 assertEquals(1, pool.getConnectionCount()); 160 assertEquals(1, pool.getHttpConnectionCount()); 161 assertEquals(0, pool.getMultiplexedConnectionCount()); 162 163 Connection recycledConnection = pool.get(httpAddress); 164 assertNull(connection.getOwner()); 165 assertEquals(connection, recycledConnection); 166 assertTrue(recycledConnection.isAlive()); 167 168 recycledConnection = pool.get(httpAddress); 169 assertNull(recycledConnection); 170 } 171 172 @Test public void getDoesNotScheduleCleanup() { 173 Connection connection = pool.get(httpAddress); 174 assertNull(connection); 175 cleanupExecutor.assertExecutionScheduled(false); 176 } 177 178 @Test public void recycleSchedulesCleanup() { 179 cleanupExecutor.assertExecutionScheduled(false); 180 pool.recycle(httpA); 181 cleanupExecutor.assertExecutionScheduled(true); 182 } 183 184 @Test public void shareSchedulesCleanup() { 185 cleanupExecutor.assertExecutionScheduled(false); 186 pool.share(spdyA); 187 cleanupExecutor.assertExecutionScheduled(true); 188 } 189 190 @Test public void poolPrefersMostRecentlyRecycled() throws Exception { 191 pool.recycle(httpA); 192 pool.recycle(httpB); 193 pool.recycle(httpC); 194 assertPooled(pool, httpC, httpB, httpA); 195 196 pool.performCleanup(); 197 assertPooled(pool, httpC, httpB); 198 } 199 200 @Test public void getSpdyConnection() throws Exception { 201 pool.share(spdyA); 202 assertSame(spdyA, pool.get(spdyAddress)); 203 assertPooled(pool, spdyA); 204 } 205 206 @Test public void getHttpConnection() throws Exception { 207 pool.recycle(httpA); 208 assertSame(httpA, pool.get(httpAddress)); 209 assertPooled(pool); 210 } 211 212 @Test public void expiredConnectionNotReturned() throws Exception { 213 pool.recycle(httpA); 214 215 // Allow enough time to pass so that the connection is now expired. 216 Thread.sleep(KEEP_ALIVE_DURATION_MS * 2); 217 218 // The connection is held, but will not be returned. 219 assertNull(pool.get(httpAddress)); 220 assertPooled(pool, httpA); 221 222 // The connection must be cleaned up. 223 pool.performCleanup(); 224 assertPooled(pool); 225 } 226 227 @Test public void maxIdleConnectionLimitIsEnforced() throws Exception { 228 pool.recycle(httpA); 229 pool.recycle(httpB); 230 pool.recycle(httpC); 231 pool.recycle(httpD); 232 assertPooled(pool, httpD, httpC, httpB, httpA); 233 234 pool.performCleanup(); 235 assertPooled(pool, httpD, httpC); 236 } 237 238 @Test public void expiredConnectionsAreEvicted() throws Exception { 239 pool.recycle(httpA); 240 pool.recycle(httpB); 241 242 // Allow enough time to pass so that the connections are now expired. 243 Thread.sleep(2 * KEEP_ALIVE_DURATION_MS); 244 assertPooled(pool, httpB, httpA); 245 246 // The connections must be cleaned up. 247 pool.performCleanup(); 248 assertPooled(pool); 249 } 250 251 @Test public void nonAliveConnectionNotReturned() throws Exception { 252 pool.recycle(httpA); 253 254 // Close the connection. It is an ex-connection. It has ceased to be. 255 httpA.getSocket().close(); 256 assertPooled(pool, httpA); 257 assertNull(pool.get(httpAddress)); 258 259 // The connection must be cleaned up. 260 pool.performCleanup(); 261 assertPooled(pool); 262 } 263 264 @Test public void differentAddressConnectionNotReturned() throws Exception { 265 pool.recycle(httpA); 266 assertNull(pool.get(spdyAddress)); 267 assertPooled(pool, httpA); 268 } 269 270 @Test public void gettingSpdyConnectionPromotesItToFrontOfQueue() throws Exception { 271 pool.share(spdyA); 272 pool.recycle(httpA); 273 assertPooled(pool, httpA, spdyA); 274 assertSame(spdyA, pool.get(spdyAddress)); 275 assertPooled(pool, spdyA, httpA); 276 } 277 278 @Test public void gettingConnectionReturnsOldestFirst() throws Exception { 279 pool.recycle(httpA); 280 pool.recycle(httpB); 281 assertSame(httpA, pool.get(httpAddress)); 282 } 283 284 @Test public void recyclingNonAliveConnectionClosesThatConnection() throws Exception { 285 httpA.getSocket().shutdownInput(); 286 pool.recycle(httpA); // Should close httpA. 287 assertTrue(httpA.getSocket().isClosed()); 288 289 // The pool should remain empty, and there is no need to schedule a cleanup. 290 assertPooled(pool); 291 cleanupExecutor.assertExecutionScheduled(false); 292 } 293 294 @Test public void shareHttpConnectionFails() throws Exception { 295 try { 296 pool.share(httpA); 297 fail(); 298 } catch (IllegalArgumentException expected) { 299 } 300 // The pool should remain empty, and there is no need to schedule a cleanup. 301 assertPooled(pool); 302 cleanupExecutor.assertExecutionScheduled(false); 303 } 304 305 @Test public void recycleSpdyConnectionDoesNothing() throws Exception { 306 pool.recycle(spdyA); 307 // The pool should remain empty, and there is no need to schedule the cleanup. 308 assertPooled(pool); 309 cleanupExecutor.assertExecutionScheduled(false); 310 } 311 312 @Test public void validateIdleSpdyConnectionTimeout() throws Exception { 313 pool.share(spdyA); 314 assertPooled(pool, spdyA); // Connection should be in the pool. 315 316 Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7)); 317 pool.performCleanup(); 318 assertPooled(pool, spdyA); // Connection should still be in the pool. 319 320 Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4)); 321 pool.performCleanup(); 322 assertPooled(pool); // Connection should have been removed. 323 } 324 325 @Test public void validateIdleHttpConnectionTimeout() throws Exception { 326 pool.recycle(httpA); 327 assertPooled(pool, httpA); // Connection should be in the pool. 328 cleanupExecutor.assertExecutionScheduled(true); 329 330 Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7)); 331 pool.performCleanup(); 332 assertPooled(pool, httpA); // Connection should still be in the pool. 333 334 Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4)); 335 pool.performCleanup(); 336 assertPooled(pool); // Connection should have been removed. 337 } 338 339 @Test public void maxConnections() throws IOException, InterruptedException { 340 // Pool should be empty. 341 assertEquals(0, pool.getConnectionCount()); 342 343 // http A should be added to the pool. 344 pool.recycle(httpA); 345 assertEquals(1, pool.getConnectionCount()); 346 assertEquals(1, pool.getHttpConnectionCount()); 347 assertEquals(0, pool.getMultiplexedConnectionCount()); 348 349 // http B should be added to the pool. 350 pool.recycle(httpB); 351 assertEquals(2, pool.getConnectionCount()); 352 assertEquals(2, pool.getHttpConnectionCount()); 353 assertEquals(0, pool.getMultiplexedConnectionCount()); 354 355 // http C should be added 356 pool.recycle(httpC); 357 assertEquals(3, pool.getConnectionCount()); 358 assertEquals(3, pool.getHttpConnectionCount()); 359 assertEquals(0, pool.getSpdyConnectionCount()); 360 361 pool.performCleanup(); 362 363 // http A should be removed by cleanup. 364 assertEquals(2, pool.getConnectionCount()); 365 assertEquals(2, pool.getHttpConnectionCount()); 366 assertEquals(0, pool.getMultiplexedConnectionCount()); 367 368 // spdy A should be added 369 pool.share(spdyA); 370 assertEquals(3, pool.getConnectionCount()); 371 assertEquals(2, pool.getHttpConnectionCount()); 372 assertEquals(1, pool.getSpdyConnectionCount()); 373 374 pool.performCleanup(); 375 376 // http B should be removed by cleanup. 377 assertEquals(2, pool.getConnectionCount()); 378 assertEquals(1, pool.getHttpConnectionCount()); 379 assertEquals(1, pool.getMultiplexedConnectionCount()); 380 381 // http C should be returned. 382 Connection recycledHttpConnection = pool.get(httpAddress); 383 recycledHttpConnection.setOwner(owner); 384 assertNotNull(recycledHttpConnection); 385 assertTrue(recycledHttpConnection.isAlive()); 386 assertEquals(1, pool.getConnectionCount()); 387 assertEquals(0, pool.getHttpConnectionCount()); 388 assertEquals(1, pool.getMultiplexedConnectionCount()); 389 390 // spdy A will be returned but also kept in the pool. 391 Connection sharedSpdyConnection = pool.get(spdyAddress); 392 assertNotNull(sharedSpdyConnection); 393 assertEquals(spdyA, sharedSpdyConnection); 394 assertEquals(1, pool.getConnectionCount()); 395 assertEquals(0, pool.getHttpConnectionCount()); 396 assertEquals(1, pool.getMultiplexedConnectionCount()); 397 398 // http C should be added to the pool 399 pool.recycle(httpC); 400 assertEquals(2, pool.getConnectionCount()); 401 assertEquals(1, pool.getHttpConnectionCount()); 402 assertEquals(1, pool.getMultiplexedConnectionCount()); 403 404 // An http connection should be removed from the pool. 405 recycledHttpConnection = pool.get(httpAddress); 406 assertNotNull(recycledHttpConnection); 407 assertTrue(recycledHttpConnection.isAlive()); 408 assertEquals(1, pool.getConnectionCount()); 409 assertEquals(0, pool.getHttpConnectionCount()); 410 assertEquals(1, pool.getMultiplexedConnectionCount()); 411 412 // spdy A will be returned but also kept in the pool. 413 sharedSpdyConnection = pool.get(spdyAddress); 414 assertEquals(spdyA, sharedSpdyConnection); 415 assertNotNull(sharedSpdyConnection); 416 assertEquals(1, pool.getConnectionCount()); 417 assertEquals(0, pool.getHttpConnectionCount()); 418 assertEquals(1, pool.getMultiplexedConnectionCount()); 419 420 // http D should be added to the pool. 421 pool.recycle(httpD); 422 assertEquals(2, pool.getConnectionCount()); 423 assertEquals(1, pool.getHttpConnectionCount()); 424 assertEquals(1, pool.getMultiplexedConnectionCount()); 425 426 // http E should be added to the pool. 427 pool.recycle(httpE); 428 assertEquals(3, pool.getConnectionCount()); 429 assertEquals(2, pool.getHttpConnectionCount()); 430 assertEquals(1, pool.getSpdyConnectionCount()); 431 432 pool.performCleanup(); 433 434 // spdy A should be removed from the pool by cleanup. 435 assertEquals(2, pool.getConnectionCount()); 436 assertEquals(2, pool.getHttpConnectionCount()); 437 assertEquals(0, pool.getMultiplexedConnectionCount()); 438 } 439 440 @Test public void connectionCleanup() throws Exception { 441 ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS); 442 443 // Add 3 connections to the pool. 444 pool.recycle(httpA); 445 pool.recycle(httpB); 446 pool.share(spdyA); 447 448 // Give the cleanup callable time to run and settle down. 449 Thread.sleep(100); 450 451 // Kill http A. 452 Util.closeQuietly(httpA.getSocket()); 453 454 assertEquals(3, pool.getConnectionCount()); 455 assertEquals(2, pool.getHttpConnectionCount()); 456 assertEquals(1, pool.getSpdyConnectionCount()); 457 458 // Http A should be removed. 459 pool.performCleanup(); 460 assertPooled(pool, spdyA, httpB); 461 assertEquals(2, pool.getConnectionCount()); 462 assertEquals(1, pool.getHttpConnectionCount()); 463 assertEquals(1, pool.getMultiplexedConnectionCount()); 464 465 // Now let enough time pass for the connections to expire. 466 Thread.sleep(2 * KEEP_ALIVE_DURATION_MS); 467 468 // All remaining connections should be removed. 469 pool.performCleanup(); 470 assertEquals(0, pool.getConnectionCount()); 471 } 472 473 @Test public void maxIdleConnectionsLimitEnforced() throws Exception { 474 ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS); 475 476 // Hit the max idle connections limit of 2. 477 pool.recycle(httpA); 478 pool.recycle(httpB); 479 Thread.sleep(100); // Give the cleanup callable time to run. 480 assertPooled(pool, httpB, httpA); 481 482 // Adding httpC bumps httpA. 483 pool.recycle(httpC); 484 Thread.sleep(100); // Give the cleanup callable time to run. 485 assertPooled(pool, httpC, httpB); 486 487 // Adding httpD bumps httpB. 488 pool.recycle(httpD); 489 Thread.sleep(100); // Give the cleanup callable time to run. 490 assertPooled(pool, httpD, httpC); 491 492 // Adding httpE bumps httpC. 493 pool.recycle(httpE); 494 Thread.sleep(100); // Give the cleanup callable time to run. 495 assertPooled(pool, httpE, httpD); 496 } 497 498 @Test public void evictAllConnections() throws Exception { 499 resetWithPoolSize(10); 500 pool.recycle(httpA); 501 Util.closeQuietly(httpA.getSocket()); // Include a closed connection in the pool. 502 pool.recycle(httpB); 503 pool.share(spdyA); 504 int connectionCount = pool.getConnectionCount(); 505 assertTrue(connectionCount == 2 || connectionCount == 3); 506 507 pool.evictAll(); 508 assertEquals(0, pool.getConnectionCount()); 509 } 510 511 @Test public void closeIfOwnedBy() throws Exception { 512 httpA.closeIfOwnedBy(owner); 513 assertFalse(httpA.isAlive()); 514 assertFalse(httpA.clearOwner()); 515 } 516 517 @Test public void closeIfOwnedByDoesNothingIfNotOwner() throws Exception { 518 httpA.closeIfOwnedBy(new Object()); 519 assertTrue(httpA.isAlive()); 520 assertTrue(httpA.clearOwner()); 521 } 522 523 @Test public void closeIfOwnedByFailsForSpdyConnections() throws Exception { 524 try { 525 spdyA.closeIfOwnedBy(owner); 526 fail(); 527 } catch (IllegalStateException expected) { 528 } 529 } 530 531 @Test public void cleanupRunnableStopsEventually() throws Exception { 532 pool.recycle(httpA); 533 pool.share(spdyA); 534 assertPooled(pool, spdyA, httpA); 535 536 // The cleanup should terminate once the pool is empty again. 537 cleanupExecutor.fakeExecute(); 538 assertPooled(pool); 539 540 cleanupExecutor.assertExecutionScheduled(false); 541 542 // Adding a new connection should cause the cleanup to start up again. 543 pool.recycle(httpB); 544 545 cleanupExecutor.assertExecutionScheduled(true); 546 547 // The cleanup should terminate once the pool is empty again. 548 cleanupExecutor.fakeExecute(); 549 assertPooled(pool); 550 } 551 552 private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception { 553 assertEquals(Arrays.asList(connections), pool.getConnections()); 554 } 555 556 /** 557 * An executor that does not actually execute anything by default. See 558 * {@link #fakeExecute()}. 559 */ 560 private static class FakeExecutor implements Executor { 561 562 private Runnable runnable; 563 564 @Override 565 public void execute(Runnable runnable) { 566 // This is a bonus assertion for the invariant: At no time should two runnables be scheduled. 567 assertNull(this.runnable); 568 this.runnable = runnable; 569 } 570 571 public void assertExecutionScheduled(boolean expected) { 572 assertEquals(expected, runnable != null); 573 } 574 575 /** 576 * Executes the runnable. 577 */ 578 public void fakeExecute() { 579 Runnable toRun = this.runnable; 580 this.runnable = null; 581 toRun.run(); 582 } 583 } 584} 585