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