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.RecordingHostnameVerifier;
19import com.squareup.okhttp.internal.SslContextBuilder;
20import com.squareup.okhttp.internal.Util;
21import com.squareup.okhttp.internal.http.HttpAuthenticator;
22import com.squareup.okhttp.mockwebserver.MockWebServer;
23import java.io.IOException;
24import java.net.InetAddress;
25import java.net.InetSocketAddress;
26import java.net.Proxy;
27import java.util.Arrays;
28import javax.net.SocketFactory;
29import javax.net.ssl.SSLContext;
30import org.junit.After;
31import org.junit.Before;
32import org.junit.Test;
33
34import static org.junit.Assert.assertEquals;
35import static org.junit.Assert.assertFalse;
36import static org.junit.Assert.assertNotNull;
37import static org.junit.Assert.assertNull;
38import static org.junit.Assert.assertSame;
39import static org.junit.Assert.assertTrue;
40import static org.junit.Assert.fail;
41
42public final class ConnectionPoolTest {
43  private static final int KEEP_ALIVE_DURATION_MS = 5000;
44  private static final SSLContext sslContext = SslContextBuilder.localhost();
45
46  private MockWebServer spdyServer;
47  private InetSocketAddress spdySocketAddress;
48  private Address spdyAddress;
49
50  private MockWebServer httpServer;
51  private Address httpAddress;
52  private InetSocketAddress httpSocketAddress;
53
54  private ConnectionPool pool;
55  private Connection httpA;
56  private Connection httpB;
57  private Connection httpC;
58  private Connection httpD;
59  private Connection httpE;
60  private Connection spdyA;
61
62  private Object owner;
63
64  @Before public void setUp() throws Exception {
65    setUp(2);
66  }
67
68  private void setUp(int poolSize) throws Exception {
69    SocketFactory socketFactory = SocketFactory.getDefault();
70
71    spdyServer = new MockWebServer();
72    httpServer = new MockWebServer();
73    spdyServer.useHttps(sslContext.getSocketFactory(), false);
74
75    httpServer.play();
76    httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), socketFactory, null,
77        null, HttpAuthenticator.SYSTEM_DEFAULT, null, Protocol.SPDY3_AND_HTTP11);
78    httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
79        httpServer.getPort());
80
81    spdyServer.play();
82    spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), socketFactory,
83        sslContext.getSocketFactory(), new RecordingHostnameVerifier(),
84        HttpAuthenticator.SYSTEM_DEFAULT, null,Protocol.SPDY3_AND_HTTP11);
85    spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
86        spdyServer.getPort());
87
88    Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
89    Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
90    pool = new ConnectionPool(poolSize, KEEP_ALIVE_DURATION_MS);
91    httpA = new Connection(pool, httpRoute);
92    httpA.connect(200, 200, null);
93    httpB = new Connection(pool, httpRoute);
94    httpB.connect(200, 200, null);
95    httpC = new Connection(pool, httpRoute);
96    httpC.connect(200, 200, null);
97    httpD = new Connection(pool, httpRoute);
98    httpD.connect(200, 200, null);
99    httpE = new Connection(pool, httpRoute);
100    httpE.connect(200, 200, null);
101    spdyA = new Connection(pool, spdyRoute);
102    spdyA.connect(20000, 20000, null);
103
104    owner = new Object();
105    httpA.setOwner(owner);
106    httpB.setOwner(owner);
107    httpC.setOwner(owner);
108    httpD.setOwner(owner);
109    httpE.setOwner(owner);
110  }
111
112  @After public void tearDown() throws Exception {
113    httpServer.shutdown();
114    spdyServer.shutdown();
115
116    Util.closeQuietly(httpA);
117    Util.closeQuietly(httpB);
118    Util.closeQuietly(httpC);
119    Util.closeQuietly(httpD);
120    Util.closeQuietly(httpE);
121    Util.closeQuietly(spdyA);
122  }
123
124  private void resetWithPoolSize(int poolSize) throws Exception {
125    tearDown();
126    setUp(poolSize);
127  }
128
129  @Test public void poolSingleHttpConnection() throws Exception {
130    resetWithPoolSize(1);
131    Connection connection = pool.get(httpAddress);
132    assertNull(connection);
133
134    connection = new Connection(
135        pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true));
136    connection.connect(200, 200, null);
137    connection.setOwner(owner);
138    assertEquals(0, pool.getConnectionCount());
139
140    pool.recycle(connection);
141    assertNull(connection.getOwner());
142    assertEquals(1, pool.getConnectionCount());
143    assertEquals(1, pool.getHttpConnectionCount());
144    assertEquals(0, pool.getSpdyConnectionCount());
145
146    Connection recycledConnection = pool.get(httpAddress);
147    assertNull(connection.getOwner());
148    assertEquals(connection, recycledConnection);
149    assertTrue(recycledConnection.isAlive());
150
151    recycledConnection = pool.get(httpAddress);
152    assertNull(recycledConnection);
153  }
154
155  @Test public void poolPrefersMostRecentlyRecycled() throws Exception {
156    pool.recycle(httpA);
157    pool.recycle(httpB);
158    pool.recycle(httpC);
159    assertPooled(pool, httpC, httpB);
160  }
161
162  @Test public void getSpdyConnection() throws Exception {
163    pool.share(spdyA);
164    assertSame(spdyA, pool.get(spdyAddress));
165    assertPooled(pool, spdyA);
166  }
167
168  @Test public void getHttpConnection() throws Exception {
169    pool.recycle(httpA);
170    assertSame(httpA, pool.get(httpAddress));
171    assertPooled(pool);
172  }
173
174  @Test public void idleConnectionNotReturned() throws Exception {
175    pool.recycle(httpA);
176    Thread.sleep(KEEP_ALIVE_DURATION_MS * 2);
177    assertNull(pool.get(httpAddress));
178    assertPooled(pool);
179  }
180
181  @Test public void maxIdleConnectionLimitIsEnforced() throws Exception {
182    pool.recycle(httpA);
183    pool.recycle(httpB);
184    pool.recycle(httpC);
185    pool.recycle(httpD);
186    assertPooled(pool, httpD, httpC);
187  }
188
189  @Test public void expiredConnectionsAreEvicted() throws Exception {
190    pool.recycle(httpA);
191    pool.recycle(httpB);
192    Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
193    pool.get(spdyAddress); // Force the cleanup callable to run.
194    assertPooled(pool);
195  }
196
197  @Test public void nonAliveConnectionNotReturned() throws Exception {
198    pool.recycle(httpA);
199    httpA.close();
200    assertNull(pool.get(httpAddress));
201    assertPooled(pool);
202  }
203
204  @Test public void differentAddressConnectionNotReturned() throws Exception {
205    pool.recycle(httpA);
206    assertNull(pool.get(spdyAddress));
207    assertPooled(pool, httpA);
208  }
209
210  @Test public void gettingSpdyConnectionPromotesItToFrontOfQueue() throws Exception {
211    pool.share(spdyA);
212    pool.recycle(httpA);
213    assertPooled(pool, httpA, spdyA);
214    assertSame(spdyA, pool.get(spdyAddress));
215    assertPooled(pool, spdyA, httpA);
216  }
217
218  @Test public void gettingConnectionReturnsOldestFirst() throws Exception {
219    pool.recycle(httpA);
220    pool.recycle(httpB);
221    assertSame(httpA, pool.get(httpAddress));
222  }
223
224  @Test public void recyclingNonAliveConnectionClosesThatConnection() throws Exception {
225    httpA.getSocket().shutdownInput();
226    pool.recycle(httpA); // Should close httpA.
227    assertTrue(httpA.getSocket().isClosed());
228  }
229
230  @Test public void shareHttpConnectionFails() throws Exception {
231    try {
232      pool.share(httpA);
233      fail();
234    } catch (IllegalArgumentException expected) {
235    }
236    assertPooled(pool);
237  }
238
239  @Test public void recycleSpdyConnectionDoesNothing() throws Exception {
240    pool.recycle(spdyA);
241    assertPooled(pool);
242  }
243
244  @Test public void validateIdleSpdyConnectionTimeout() throws Exception {
245    pool.share(spdyA);
246    Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7));
247    assertNull(pool.get(httpAddress));
248    assertPooled(pool, spdyA); // Connection should still be in the pool.
249    Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4));
250    assertNull(pool.get(httpAddress));
251    assertPooled(pool);
252  }
253
254  @Test public void validateIdleHttpConnectionTimeout() throws Exception {
255    pool.recycle(httpA);
256    Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7));
257    assertNull(pool.get(spdyAddress));
258    assertPooled(pool, httpA); // Connection should still be in the pool.
259    Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4));
260    assertNull(pool.get(spdyAddress));
261    assertPooled(pool);
262  }
263
264  @Test public void maxConnections() throws IOException, InterruptedException {
265    // Pool should be empty.
266    assertEquals(0, pool.getConnectionCount());
267
268    // http A should be added to the pool.
269    pool.recycle(httpA);
270    assertEquals(1, pool.getConnectionCount());
271    assertEquals(1, pool.getHttpConnectionCount());
272    assertEquals(0, pool.getSpdyConnectionCount());
273
274    // http B should be added to the pool.
275    pool.recycle(httpB);
276    assertEquals(2, pool.getConnectionCount());
277    assertEquals(2, pool.getHttpConnectionCount());
278    assertEquals(0, pool.getSpdyConnectionCount());
279
280    // http C should be added and http A should be removed.
281    pool.recycle(httpC);
282    Thread.sleep(50);
283    assertEquals(2, pool.getConnectionCount());
284    assertEquals(2, pool.getHttpConnectionCount());
285    assertEquals(0, pool.getSpdyConnectionCount());
286
287    // spdy A should be added and http B should be removed.
288    pool.share(spdyA);
289    Thread.sleep(50);
290    assertEquals(2, pool.getConnectionCount());
291    assertEquals(1, pool.getHttpConnectionCount());
292    assertEquals(1, pool.getSpdyConnectionCount());
293
294    // http C should be removed from the pool.
295    Connection recycledHttpConnection = pool.get(httpAddress);
296    recycledHttpConnection.setOwner(owner);
297    assertNotNull(recycledHttpConnection);
298    assertTrue(recycledHttpConnection.isAlive());
299    assertEquals(1, pool.getConnectionCount());
300    assertEquals(0, pool.getHttpConnectionCount());
301    assertEquals(1, pool.getSpdyConnectionCount());
302
303    // spdy A will be returned and kept in the pool.
304    Connection sharedSpdyConnection = pool.get(spdyAddress);
305    assertNotNull(sharedSpdyConnection);
306    assertEquals(spdyA, sharedSpdyConnection);
307    assertEquals(1, pool.getConnectionCount());
308    assertEquals(0, pool.getHttpConnectionCount());
309    assertEquals(1, pool.getSpdyConnectionCount());
310
311    // Nothing should change.
312    pool.recycle(httpC);
313    Thread.sleep(50);
314    assertEquals(2, pool.getConnectionCount());
315    assertEquals(1, pool.getHttpConnectionCount());
316    assertEquals(1, pool.getSpdyConnectionCount());
317
318    // An http connection should be removed from the pool.
319    recycledHttpConnection = pool.get(httpAddress);
320    assertNotNull(recycledHttpConnection);
321    assertTrue(recycledHttpConnection.isAlive());
322    assertEquals(1, pool.getConnectionCount());
323    assertEquals(0, pool.getHttpConnectionCount());
324    assertEquals(1, pool.getSpdyConnectionCount());
325
326    // spdy A will be returned and kept in the pool. Pool shouldn't change.
327    sharedSpdyConnection = pool.get(spdyAddress);
328    assertEquals(spdyA, sharedSpdyConnection);
329    assertNotNull(sharedSpdyConnection);
330    assertEquals(1, pool.getConnectionCount());
331    assertEquals(0, pool.getHttpConnectionCount());
332    assertEquals(1, pool.getSpdyConnectionCount());
333
334    // http D should be added to the pool.
335    pool.recycle(httpD);
336    Thread.sleep(50);
337    assertEquals(2, pool.getConnectionCount());
338    assertEquals(1, pool.getHttpConnectionCount());
339    assertEquals(1, pool.getSpdyConnectionCount());
340
341    // http E should be added to the pool. spdy A should be removed from the pool.
342    pool.recycle(httpE);
343    Thread.sleep(50);
344    assertEquals(2, pool.getConnectionCount());
345    assertEquals(2, pool.getHttpConnectionCount());
346    assertEquals(0, pool.getSpdyConnectionCount());
347  }
348
349  @Test public void connectionCleanup() throws IOException, InterruptedException {
350    ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
351
352    // Add 3 connections to the pool.
353    pool.recycle(httpA);
354    pool.recycle(httpB);
355    pool.share(spdyA);
356    assertEquals(3, pool.getConnectionCount());
357    assertEquals(2, pool.getHttpConnectionCount());
358    assertEquals(1, pool.getSpdyConnectionCount());
359
360    // Kill http A.
361    Util.closeQuietly(httpA);
362
363    // Force pool to run a clean up.
364    assertNotNull(pool.get(spdyAddress));
365    Thread.sleep(50);
366
367    assertEquals(2, pool.getConnectionCount());
368    assertEquals(1, pool.getHttpConnectionCount());
369    assertEquals(1, pool.getSpdyConnectionCount());
370
371    Thread.sleep(KEEP_ALIVE_DURATION_MS);
372    // Force pool to run a clean up.
373    assertNull(pool.get(httpAddress));
374    assertNull(pool.get(spdyAddress));
375
376    Thread.sleep(50);
377
378    assertEquals(0, pool.getConnectionCount());
379    assertEquals(0, pool.getHttpConnectionCount());
380    assertEquals(0, pool.getSpdyConnectionCount());
381  }
382
383  // Tests to demonstrate Android bug http://b/18369687 and the solution to it.
384  @Test public void connectionCleanup_draining() throws IOException, InterruptedException {
385    ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
386
387    // Add 3 connections to the pool.
388    pool.recycle(httpA);
389    pool.recycle(httpB);
390    pool.share(spdyA);
391    assertEquals(3, pool.getConnectionCount());
392    assertEquals(2, pool.getHttpConnectionCount());
393    assertEquals(1, pool.getSpdyConnectionCount());
394
395    // With no method calls made to the pool it will not clean up any connections.
396    Thread.sleep(KEEP_ALIVE_DURATION_MS * 5);
397    assertEquals(3, pool.getConnectionCount());
398    assertEquals(2, pool.getHttpConnectionCount());
399    assertEquals(1, pool.getSpdyConnectionCount());
400
401    // Change the pool into a mode that will clean up connections.
402    pool.enterDrainMode();
403
404    // Give the drain thread a chance to run.
405    for (int i = 0; i < 5; i++) {
406      Thread.sleep(KEEP_ALIVE_DURATION_MS);
407      if (pool.isDrained()) {
408        break;
409      }
410    }
411
412    // All connections should have drained.
413    assertEquals(0, pool.getConnectionCount());
414  }
415
416  @Test public void evictAllConnections() throws Exception {
417    resetWithPoolSize(10);
418    pool.recycle(httpA);
419    Util.closeQuietly(httpA); // Include a closed connection in the pool.
420    pool.recycle(httpB);
421    pool.share(spdyA);
422    int connectionCount = pool.getConnectionCount();
423    assertTrue(connectionCount == 2 || connectionCount == 3);
424
425    pool.evictAll();
426    assertEquals(0, pool.getConnectionCount());
427  }
428
429  @Test public void closeIfOwnedBy() throws Exception {
430    httpA.closeIfOwnedBy(owner);
431    assertFalse(httpA.isAlive());
432    assertFalse(httpA.clearOwner());
433  }
434
435  @Test public void closeIfOwnedByDoesNothingIfNotOwner() throws Exception {
436    httpA.closeIfOwnedBy(new Object());
437    assertTrue(httpA.isAlive());
438    assertTrue(httpA.clearOwner());
439  }
440
441  @Test public void closeIfOwnedByFailsForSpdyConnections() throws Exception {
442    try {
443      spdyA.closeIfOwnedBy(owner);
444      fail();
445    } catch (IllegalStateException expected) {
446    }
447  }
448
449  private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception {
450    assertEquals(Arrays.asList(connections), pool.getConnections());
451  }
452}
453