275 lines
9.5 KiB
Java
275 lines
9.5 KiB
Java
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright ownership.
|
|
* The ASF licenses this file to You 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.Platform;
|
|
import com.squareup.okhttp.internal.Util;
|
|
import java.net.SocketException;
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.ListIterator;
|
|
import java.util.concurrent.Callable;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/**
|
|
* Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP
|
|
* requests that share the same {@link com.squareup.okhttp.Address} may share a
|
|
* {@link com.squareup.okhttp.Connection}. This class implements the policy of
|
|
* which connections to keep open for future use.
|
|
*
|
|
* <p>The {@link #getDefault() system-wide default} uses system properties for
|
|
* tuning parameters:
|
|
* <ul>
|
|
* <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
|
|
* pooled at all. Default is true.
|
|
* <li>{@code http.maxConnections} maximum number of idle connections to
|
|
* each to keep in the pool. Default is 5.
|
|
* <li>{@code http.keepAliveDuration} Time in milliseconds to keep the
|
|
* connection alive in the pool before closing it. Default is 5 minutes.
|
|
* This property isn't used by {@code HttpURLConnection}.
|
|
* </ul>
|
|
*
|
|
* <p>The default instance <i>doesn't</i> adjust its configuration as system
|
|
* properties are changed. This assumes that the applications that set these
|
|
* parameters do so before making HTTP connections, and that this class is
|
|
* initialized lazily.
|
|
*/
|
|
public class ConnectionPool {
|
|
private static final int MAX_CONNECTIONS_TO_CLEANUP = 2;
|
|
private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
|
|
|
|
private static final ConnectionPool systemDefault;
|
|
|
|
static {
|
|
String keepAlive = System.getProperty("http.keepAlive");
|
|
String keepAliveDuration = System.getProperty("http.keepAliveDuration");
|
|
String maxIdleConnections = System.getProperty("http.maxConnections");
|
|
long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration)
|
|
: DEFAULT_KEEP_ALIVE_DURATION_MS;
|
|
if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
|
|
systemDefault = new ConnectionPool(0, keepAliveDurationMs);
|
|
} else if (maxIdleConnections != null) {
|
|
systemDefault = new ConnectionPool(Integer.parseInt(maxIdleConnections), keepAliveDurationMs);
|
|
} else {
|
|
systemDefault = new ConnectionPool(5, keepAliveDurationMs);
|
|
}
|
|
}
|
|
|
|
/** The maximum number of idle connections for each address. */
|
|
private final int maxIdleConnections;
|
|
private final long keepAliveDurationNs;
|
|
|
|
private final LinkedList<Connection> connections = new LinkedList<Connection>();
|
|
|
|
/** We use a single background thread to cleanup expired connections. */
|
|
private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
|
|
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
|
|
Util.daemonThreadFactory("OkHttp ConnectionPool"));
|
|
private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
|
|
@Override public Void call() throws Exception {
|
|
List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
|
|
int idleConnectionCount = 0;
|
|
synchronized (ConnectionPool.this) {
|
|
for (ListIterator<Connection> i = connections.listIterator(connections.size());
|
|
i.hasPrevious(); ) {
|
|
Connection connection = i.previous();
|
|
if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) {
|
|
i.remove();
|
|
expiredConnections.add(connection);
|
|
if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break;
|
|
} else if (connection.isIdle()) {
|
|
idleConnectionCount++;
|
|
}
|
|
}
|
|
|
|
for (ListIterator<Connection> i = connections.listIterator(connections.size());
|
|
i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) {
|
|
Connection connection = i.previous();
|
|
if (connection.isIdle()) {
|
|
expiredConnections.add(connection);
|
|
i.remove();
|
|
--idleConnectionCount;
|
|
}
|
|
}
|
|
}
|
|
for (Connection expiredConnection : expiredConnections) {
|
|
Util.closeQuietly(expiredConnection);
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
|
|
this.maxIdleConnections = maxIdleConnections;
|
|
this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
|
|
}
|
|
|
|
/**
|
|
* Returns a snapshot of the connections in this pool, ordered from newest to
|
|
* oldest. Waits for the cleanup callable to run if it is currently scheduled.
|
|
*/
|
|
List<Connection> getConnections() {
|
|
waitForCleanupCallableToRun();
|
|
synchronized (this) {
|
|
return new ArrayList<Connection>(connections);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Blocks until the executor service has processed all currently enqueued
|
|
* jobs.
|
|
*/
|
|
private void waitForCleanupCallableToRun() {
|
|
try {
|
|
executorService.submit(new Runnable() {
|
|
@Override public void run() {
|
|
}
|
|
}).get();
|
|
} catch (Exception e) {
|
|
throw new AssertionError();
|
|
}
|
|
}
|
|
|
|
public static ConnectionPool getDefault() {
|
|
return systemDefault;
|
|
}
|
|
|
|
/** Returns total number of connections in the pool. */
|
|
public synchronized int getConnectionCount() {
|
|
return connections.size();
|
|
}
|
|
|
|
/** Returns total number of spdy connections in the pool. */
|
|
public synchronized int getSpdyConnectionCount() {
|
|
int total = 0;
|
|
for (Connection connection : connections) {
|
|
if (connection.isSpdy()) total++;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/** Returns total number of http connections in the pool. */
|
|
public synchronized int getHttpConnectionCount() {
|
|
int total = 0;
|
|
for (Connection connection : connections) {
|
|
if (!connection.isSpdy()) total++;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
|
|
public synchronized Connection get(Address address) {
|
|
Connection foundConnection = null;
|
|
for (ListIterator<Connection> i = connections.listIterator(connections.size());
|
|
i.hasPrevious(); ) {
|
|
Connection connection = i.previous();
|
|
if (!connection.getRoute().getAddress().equals(address)
|
|
|| !connection.isAlive()
|
|
|| System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
|
|
continue;
|
|
}
|
|
i.remove();
|
|
if (!connection.isSpdy()) {
|
|
try {
|
|
Platform.get().tagSocket(connection.getSocket());
|
|
} catch (SocketException e) {
|
|
Util.closeQuietly(connection);
|
|
// When unable to tag, skip recycling and close
|
|
Platform.get().logW("Unable to tagSocket(): " + e);
|
|
continue;
|
|
}
|
|
}
|
|
foundConnection = connection;
|
|
break;
|
|
}
|
|
|
|
if (foundConnection != null && foundConnection.isSpdy()) {
|
|
connections.addFirst(foundConnection); // Add it back after iteration.
|
|
}
|
|
|
|
executorService.submit(connectionsCleanupCallable);
|
|
return foundConnection;
|
|
}
|
|
|
|
/**
|
|
* Gives {@code connection} to the pool. The pool may store the connection,
|
|
* or close it, as its policy describes.
|
|
*
|
|
* <p>It is an error to use {@code connection} after calling this method.
|
|
*/
|
|
public void recycle(Connection connection) {
|
|
if (connection.isSpdy()) {
|
|
return;
|
|
}
|
|
|
|
if (!connection.isAlive()) {
|
|
Util.closeQuietly(connection);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Platform.get().untagSocket(connection.getSocket());
|
|
} catch (SocketException e) {
|
|
// When unable to remove tagging, skip recycling and close.
|
|
Platform.get().logW("Unable to untagSocket(): " + e);
|
|
Util.closeQuietly(connection);
|
|
return;
|
|
}
|
|
|
|
synchronized (this) {
|
|
connections.addFirst(connection);
|
|
connection.resetIdleStartTime();
|
|
}
|
|
|
|
executorService.submit(connectionsCleanupCallable);
|
|
}
|
|
|
|
/**
|
|
* Shares the SPDY connection with the pool. Callers to this method may
|
|
* continue to use {@code connection}.
|
|
*/
|
|
public void maybeShare(Connection connection) {
|
|
executorService.submit(connectionsCleanupCallable);
|
|
if (!connection.isSpdy()) {
|
|
// Only SPDY connections are sharable.
|
|
return;
|
|
}
|
|
if (connection.isAlive()) {
|
|
synchronized (this) {
|
|
connections.addFirst(connection);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Close and remove all connections in the pool. */
|
|
public void evictAll() {
|
|
List<Connection> connections;
|
|
synchronized (this) {
|
|
connections = new ArrayList<Connection>(this.connections);
|
|
this.connections.clear();
|
|
}
|
|
|
|
for (Connection connection : connections) {
|
|
Util.closeQuietly(connection);
|
|
}
|
|
}
|
|
}
|