1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17package com.squareup.okhttp;
18
19import com.squareup.okhttp.internal.Platform;
20import com.squareup.okhttp.internal.Util;
21import java.net.SocketException;
22import java.util.ArrayList;
23import java.util.LinkedList;
24import java.util.List;
25import java.util.ListIterator;
26import java.util.concurrent.ExecutorService;
27import java.util.concurrent.LinkedBlockingQueue;
28import java.util.concurrent.ThreadPoolExecutor;
29import java.util.concurrent.TimeUnit;
30
31/**
32 * Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP
33 * requests that share the same {@link com.squareup.okhttp.Address} may share a
34 * {@link com.squareup.okhttp.Connection}. This class implements the policy of
35 * which connections to keep open for future use.
36 *
37 * <p>The {@link #getDefault() system-wide default} uses system properties for
38 * tuning parameters:
39 * <ul>
40 *     <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
41 *         pooled at all. Default is true.
42 *     <li>{@code http.maxConnections} maximum number of idle connections to
43 *         each to keep in the pool. Default is 5.
44 *     <li>{@code http.keepAliveDuration} Time in milliseconds to keep the
45 *         connection alive in the pool before closing it. Default is 5 minutes.
46 *         This property isn't used by {@code HttpURLConnection}.
47 * </ul>
48 *
49 * <p>The default instance <i>doesn't</i> adjust its configuration as system
50 * properties are changed. This assumes that the applications that set these
51 * parameters do so before making HTTP connections, and that this class is
52 * initialized lazily.
53 */
54public class ConnectionPool {
55  private static final int MAX_CONNECTIONS_TO_CLEANUP = 2;
56  private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
57
58  private static final ConnectionPool systemDefault;
59
60  static {
61    String keepAlive = System.getProperty("http.keepAlive");
62    String keepAliveDuration = System.getProperty("http.keepAliveDuration");
63    String maxIdleConnections = System.getProperty("http.maxConnections");
64    long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration)
65        : DEFAULT_KEEP_ALIVE_DURATION_MS;
66    if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
67      systemDefault = new ConnectionPool(0, keepAliveDurationMs);
68    } else if (maxIdleConnections != null) {
69      systemDefault = new ConnectionPool(Integer.parseInt(maxIdleConnections), keepAliveDurationMs);
70    } else {
71      systemDefault = new ConnectionPool(5, keepAliveDurationMs);
72    }
73  }
74
75  /** The maximum number of idle connections for each address. */
76  private final int maxIdleConnections;
77  private final long keepAliveDurationNs;
78
79  private final LinkedList<Connection> connections = new LinkedList<Connection>();
80
81  /** We use a single background thread to cleanup expired connections. */
82  private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
83      60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
84      Util.threadFactory("OkHttp ConnectionPool", true));
85
86  private enum CleanMode {
87    /**
88     * Connection clean up is driven by usage of the pool. Each usage of the pool can schedule a
89     * clean up. A pool left in this state and unused may contain idle connections indefinitely.
90     */
91    NORMAL,
92    /**
93     * Entered when a pool has been orphaned and is not expected to receive more usage, except for
94     * references held by existing connections. See {@link #enterDrainMode()}.
95     * A thread runs periodically to close idle connections in the pool until the pool is empty and
96     * then the state moves to {@link #DRAINED}.
97     */
98    DRAINING,
99    /**
100     * The pool is empty and no clean-up is taking place. Connections may still be added to the
101     * pool due to latent references to the pool, in which case the pool re-enters
102     * {@link #DRAINING}. If the pool is DRAINED and no longer referenced it is safe to be garbage
103     * collected.
104     */
105    DRAINED
106  }
107  /** The current mode for cleaning connections in the pool */
108  private CleanMode cleanMode = CleanMode.NORMAL;
109
110  // A scheduled drainModeRunnable keeps a reference to the enclosing ConnectionPool,
111  // preventing the ConnectionPool from being garbage collected before all held connections have
112  // been explicitly closed. If this was not the case any open connections in the pool would trigger
113  // StrictMode violations in Android when they were garbage collected. http://b/18369687
114  private final Runnable drainModeRunnable = new Runnable() {
115    @Override public void run() {
116      // Close any connections we can.
117      connectionsCleanupRunnable.run();
118
119      synchronized (ConnectionPool.this) {
120        // See whether we should continue checking the connection pool.
121        if (connections.size() > 0) {
122          // Pause to avoid checking too regularly, which would drain the battery on mobile
123          // devices. The wait() surrenders the pool monitor and will not block other calls.
124          try {
125            // Use the keep alive duration as a rough indicator of a good check interval.
126            long keepAliveDurationMillis = keepAliveDurationNs / (1000 * 1000);
127            ConnectionPool.this.wait(keepAliveDurationMillis);
128          } catch (InterruptedException e) {
129            // Ignored.
130          }
131
132          // Reschedule "this" to perform another clean-up.
133          executorService.execute(this);
134        } else {
135          cleanMode = CleanMode.DRAINED;
136        }
137      }
138    }
139  };
140
141  private final Runnable connectionsCleanupRunnable = new Runnable() {
142    @Override public void run() {
143      List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
144      int idleConnectionCount = 0;
145      synchronized (ConnectionPool.this) {
146        for (ListIterator<Connection> i = connections.listIterator(connections.size());
147            i.hasPrevious(); ) {
148          Connection connection = i.previous();
149          if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) {
150            i.remove();
151            expiredConnections.add(connection);
152            if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break;
153          } else if (connection.isIdle()) {
154            idleConnectionCount++;
155          }
156        }
157
158        for (ListIterator<Connection> i = connections.listIterator(connections.size());
159            i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) {
160          Connection connection = i.previous();
161          if (connection.isIdle()) {
162            expiredConnections.add(connection);
163            i.remove();
164            --idleConnectionCount;
165          }
166        }
167      }
168      for (Connection expiredConnection : expiredConnections) {
169        Util.closeQuietly(expiredConnection);
170      }
171    }
172  };
173
174  public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
175    this.maxIdleConnections = maxIdleConnections;
176    this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
177  }
178
179  /**
180   * Returns a snapshot of the connections in this pool, ordered from newest to
181   * oldest. Waits for the cleanup callable to run if it is currently scheduled.
182   * Only use in tests.
183   */
184  List<Connection> getConnections() {
185    waitForCleanupCallableToRun();
186    synchronized (this) {
187      return new ArrayList<Connection>(connections);
188    }
189  }
190
191  /**
192   * Blocks until the executor service has processed all currently enqueued
193   * jobs.
194   */
195  private void waitForCleanupCallableToRun() {
196    try {
197      executorService.submit(new Runnable() {
198        @Override public void run() {
199        }
200      }).get();
201    } catch (Exception e) {
202      throw new AssertionError();
203    }
204  }
205
206  public static ConnectionPool getDefault() {
207    return systemDefault;
208  }
209
210  /** Returns total number of connections in the pool. */
211  public synchronized int getConnectionCount() {
212    return connections.size();
213  }
214
215  /** Returns total number of spdy connections in the pool. */
216  public synchronized int getSpdyConnectionCount() {
217    int total = 0;
218    for (Connection connection : connections) {
219      if (connection.isSpdy()) total++;
220    }
221    return total;
222  }
223
224  /** Returns total number of http connections in the pool. */
225  public synchronized int getHttpConnectionCount() {
226    int total = 0;
227    for (Connection connection : connections) {
228      if (!connection.isSpdy()) total++;
229    }
230    return total;
231  }
232
233  /** Returns a recycled connection to {@code address}, or null if no such connection exists. */
234  public synchronized Connection get(Address address) {
235    Connection foundConnection = null;
236    for (ListIterator<Connection> i = connections.listIterator(connections.size());
237        i.hasPrevious(); ) {
238      Connection connection = i.previous();
239      if (!connection.getRoute().getAddress().equals(address)
240          || !connection.isAlive()
241          || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
242        continue;
243      }
244      i.remove();
245      if (!connection.isSpdy()) {
246        try {
247          Platform.get().tagSocket(connection.getSocket());
248        } catch (SocketException e) {
249          Util.closeQuietly(connection);
250          // When unable to tag, skip recycling and close
251          Platform.get().logW("Unable to tagSocket(): " + e);
252          continue;
253        }
254      }
255      foundConnection = connection;
256      break;
257    }
258
259    if (foundConnection != null && foundConnection.isSpdy()) {
260      connections.addFirst(foundConnection); // Add it back after iteration.
261    }
262
263    scheduleCleanupAsRequired();
264    return foundConnection;
265  }
266
267  /**
268   * Gives {@code connection} to the pool. The pool may store the connection,
269   * or close it, as its policy describes.
270   *
271   * <p>It is an error to use {@code connection} after calling this method.
272   */
273  public void recycle(Connection connection) {
274    if (connection.isSpdy()) {
275      return;
276    }
277
278    if (!connection.clearOwner()) {
279      return; // This connection isn't eligible for reuse.
280    }
281
282    if (!connection.isAlive()) {
283      Util.closeQuietly(connection);
284      return;
285    }
286
287    try {
288      Platform.get().untagSocket(connection.getSocket());
289    } catch (SocketException e) {
290      // When unable to remove tagging, skip recycling and close.
291      Platform.get().logW("Unable to untagSocket(): " + e);
292      Util.closeQuietly(connection);
293      return;
294    }
295
296    synchronized (this) {
297      connections.addFirst(connection);
298      connection.incrementRecycleCount();
299      connection.resetIdleStartTime();
300      scheduleCleanupAsRequired();
301    }
302
303  }
304
305  /**
306   * Shares the SPDY connection with the pool. Callers to this method may
307   * continue to use {@code connection}.
308   */
309  public void share(Connection connection) {
310    if (!connection.isSpdy()) throw new IllegalArgumentException();
311    if (connection.isAlive()) {
312      synchronized (this) {
313        connections.addFirst(connection);
314        scheduleCleanupAsRequired();
315      }
316    }
317  }
318
319  /** Close and remove all connections in the pool. */
320  public void evictAll() {
321    List<Connection> connections;
322    synchronized (this) {
323      connections = new ArrayList<Connection>(this.connections);
324      this.connections.clear();
325    }
326
327    for (int i = 0, size = connections.size(); i < size; i++) {
328      Util.closeQuietly(connections.get(i));
329    }
330  }
331
332  /**
333   * A less abrupt way of draining the pool than {@link #evictAll()}. For use when the pool
334   * may still be referenced by active shared connections which cannot safely be closed.
335   */
336  public void enterDrainMode() {
337    synchronized(this) {
338      cleanMode = CleanMode.DRAINING;
339      executorService.execute(drainModeRunnable);
340    }
341  }
342
343  public boolean isDrained() {
344    synchronized(this) {
345      return cleanMode == CleanMode.DRAINED;
346    }
347  }
348
349  // Callers must synchronize on "this".
350  private void scheduleCleanupAsRequired() {
351    switch (cleanMode) {
352      case NORMAL:
353        executorService.execute(connectionsCleanupRunnable);
354        break;
355      case DRAINING:
356        // Do nothing -drainModeRunnable is already scheduled, and will reschedules itself as
357        // needed.
358        break;
359      case DRAINED:
360        // A new connection has potentially been offered up to a drained pool. Restart the drain.
361        cleanMode = CleanMode.DRAINING;
362        executorService.execute(drainModeRunnable);
363        break;
364    }
365  }
366}
367