diff --git a/src/java/org/apache/http/conn/HttpHostConnection.java b/src/java/org/apache/http/conn/HttpHostConnection.java
index cfbfd471f..c9832af8e 100644
--- a/src/java/org/apache/http/conn/HttpHostConnection.java
+++ b/src/java/org/apache/http/conn/HttpHostConnection.java
@@ -33,9 +33,13 @@ import java.io.IOException;
import java.net.SocketException;
import org.apache.http.HttpClientConnection;
+import org.apache.http.HttpInetConnection;
+import org.apache.http.HttpResponse;
import org.apache.http.params.HttpParams;
-public interface HttpHostConnection extends HttpClientConnection {
+public interface HttpHostConnection extends HttpClientConnection, HttpInetConnection {
+
+ void setHttpConnectionManager(HttpConnectionManager manager);
HostConfiguration getHostConfiguration();
@@ -45,4 +49,14 @@ public interface HttpHostConnection extends HttpClientConnection {
void setSocketTimeout(int timeout) throws SocketException;
+ HttpResponse getLastResponse();
+
+ void setLastResponse(HttpResponse response);
+
+ void setLocked(boolean locked);
+
+ boolean isLocked();
+
+ void releaseConnection();
+
}
diff --git a/src/java/org/apache/http/conn/impl/DefaultHttpHostConnection.java b/src/java/org/apache/http/conn/impl/DefaultHttpHostConnection.java
index bd35ca317..e1a5a153f 100644
--- a/src/java/org/apache/http/conn/impl/DefaultHttpHostConnection.java
+++ b/src/java/org/apache/http/conn/impl/DefaultHttpHostConnection.java
@@ -67,7 +67,7 @@ public class DefaultHttpHostConnection
private static final Log LOG = LogFactory.getLog(DefaultHttpHostConnection.class);
/** the connection manager that created this connection or null */
- private final HttpConnectionManager manager;
+ private HttpConnectionManager manager;
private HostConfiguration hostconf;
@@ -80,11 +80,14 @@ public class DefaultHttpHostConnection
private HttpResponse lastResponse;
- public DefaultHttpHostConnection(final HttpConnectionManager manager) {
+ public DefaultHttpHostConnection() {
super();
- this.manager = manager;
}
+ public void setHttpConnectionManager(final HttpConnectionManager manager) {
+ this.manager = manager;
+ }
+
public void setHostConfiguration(final HostConfiguration hostconf) {
assertNotOpen();
this.hostconf = hostconf;
@@ -246,7 +249,7 @@ public class DefaultHttpHostConnection
*
* @since 3.0
*/
- protected boolean isLocked() {
+ public boolean isLocked() {
return locked;
}
@@ -259,7 +262,7 @@ public class DefaultHttpHostConnection
*
* @since 3.0
*/
- protected void setLocked(boolean locked) {
+ public void setLocked(boolean locked) {
this.locked = locked;
}
diff --git a/src/java/org/apache/http/conn/impl/IdleConnectionHandler.java b/src/java/org/apache/http/conn/impl/IdleConnectionHandler.java
new file mode 100644
index 000000000..654453cb9
--- /dev/null
+++ b/src/java/org/apache/http/conn/impl/IdleConnectionHandler.java
@@ -0,0 +1,130 @@
+/*
+ * $HeadURL$
+ * $Revision$
+ * $Date$
+ *
+ * ====================================================================
+ *
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ *
This class is not synchronized.
+ * + * @see org.apache.commons.httpclient.HttpConnectionManager#closeIdleConnections(long) + * + * @since 3.0 + */ +public class IdleConnectionHandler { + + private static final Log LOG = LogFactory.getLog(IdleConnectionHandler.class); + + /** Holds connections and the time they were added. */ + private Map connectionToAdded = new HashMap(); + + /** + * + */ + public IdleConnectionHandler() { + super(); + } + + /** + * Registers the given connection with this handler. The connection will be held until + * {@link #remove(HttpConnection)} or {@link #closeIdleConnections(long)} is called. + * + * @param connection the connection to add + * + * @see #remove(HttpConnection) + */ + public void add(HttpHostConnection connection) { + + Long timeAdded = new Long(System.currentTimeMillis()); + + if (LOG.isDebugEnabled()) { + LOG.debug("Adding connection at: " + timeAdded); + } + + connectionToAdded.put(connection, timeAdded); + } + + /** + * Removes the given connection from the list of connections to be closed when idle. + * @param connection + */ + public void remove(HttpHostConnection connection) { + connectionToAdded.remove(connection); + } + + /** + * Removes all connections referenced by this handler. + */ + public void removeAll() { + this.connectionToAdded.clear(); + } + + /** + * Closes connections that have been idle for at least the given amount of time. + * + * @param idleTime the minimum idle time, in milliseconds, for connections to be closed + */ + public void closeIdleConnections(long idleTime) { + + // the latest time for which connections will be closed + long idleTimeout = System.currentTimeMillis() - idleTime; + + if (LOG.isDebugEnabled()) { + LOG.debug("Checking for connections, idleTimeout: " + idleTimeout); + } + + Iterator connectionIter = connectionToAdded.keySet().iterator(); + + while (connectionIter.hasNext()) { + HttpHostConnection conn = (HttpHostConnection) connectionIter.next(); + Long connectionTime = (Long) connectionToAdded.get(conn); + if (connectionTime.longValue() <= idleTimeout) { + if (LOG.isDebugEnabled()) { + LOG.debug("Closing connection, connection time: " + connectionTime); + } + connectionIter.remove(); + try { + conn.close(); + } catch (IOException ex) { + LOG.debug("I/O error closing connection", ex); + } + } + } + } +} diff --git a/src/java/org/apache/http/conn/impl/MultiThreadedHttpConnectionManager.java b/src/java/org/apache/http/conn/impl/MultiThreadedHttpConnectionManager.java new file mode 100644 index 000000000..43e0f7f75 --- /dev/null +++ b/src/java/org/apache/http/conn/impl/MultiThreadedHttpConnectionManager.java @@ -0,0 +1,1201 @@ +/* + * $HeadURL$ + * $Revision$ + * $Date$ + * + * ==================================================================== + * + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + *A static reference to the connection manager will also be stored. To ensure that + * the connection manager can be GCed {@link #removeReferenceToConnection(HttpHostConnection)} + * should be called for all connections that the connection manager is storing a reference + * to.
+ * + * @param connection the connection to create a reference for + * @param hostConfiguration the connection's host config + * @param connectionPool the connection pool that created the connection + * + * @see #removeReferenceToConnection(HttpHostConnection) + */ + private static void storeReferenceToConnection( + HttpConnectionWithReference connection, + HostConfiguration hostConfiguration, + ConnectionPool connectionPool + ) { + + ConnectionSource source = new ConnectionSource(); + source.connectionPool = connectionPool; + source.hostConfiguration = hostConfiguration; + + synchronized (REFERENCE_TO_CONNECTION_SOURCE) { + + // start the reference queue thread if needed + if (REFERENCE_QUEUE_THREAD == null) { + REFERENCE_QUEUE_THREAD = new ReferenceQueueThread(); + REFERENCE_QUEUE_THREAD.start(); + } + + REFERENCE_TO_CONNECTION_SOURCE.put( + connection.reference, + source + ); + } + } + + /** + * Closes and releases all connections currently checked out of the given connection pool. + * @param connectionPool the connection pool to shutdown the connections for + */ + private static void shutdownCheckedOutConnections(ConnectionPool connectionPool) { + + // keep a list of the connections to be closed + ArrayList connectionsToClose = new ArrayList(); + + synchronized (REFERENCE_TO_CONNECTION_SOURCE) { + + Iterator referenceIter = REFERENCE_TO_CONNECTION_SOURCE.keySet().iterator(); + while (referenceIter.hasNext()) { + Reference ref = (Reference) referenceIter.next(); + ConnectionSource source = + (ConnectionSource) REFERENCE_TO_CONNECTION_SOURCE.get(ref); + if (source.connectionPool == connectionPool) { + referenceIter.remove(); + HttpHostConnection connection = (HttpHostConnection) ref.get(); + if (connection != null) { + connectionsToClose.add(connection); + } + } + } + } + + // close and release the connections outside of the synchronized block to + // avoid holding the lock for too long + for (Iterator i = connectionsToClose.iterator(); i.hasNext();) { + HttpHostConnection connection = (HttpHostConnection) i.next(); + closeConnection(connection); + // remove the reference to the connection manager. this ensures + // that the we don't accidentally end up here again + connection.setHttpConnectionManager(null); + connection.releaseConnection(); + } + } + + /** + * Removes the reference being stored for the given connection. This method should be called + * when the connection manager again has a direct reference to the connection. + * + * @param connection the connection to remove the reference for + * + * @see #storeReferenceToConnection(HttpHostConnection, HostConfiguration, ConnectionPool) + */ + private static void removeReferenceToConnection(HttpConnectionWithReference connection) { + + synchronized (REFERENCE_TO_CONNECTION_SOURCE) { + REFERENCE_TO_CONNECTION_SOURCE.remove(connection.reference); + } + } + + + // ----------------------------------------------------- Instance Variables + + /** + * Collection of parameters associated with this connection manager. + */ + private HttpParams params = new DefaultHttpParams(); + + /** Connection Pool */ + private ConnectionPool connectionPool; + + private boolean shutdown = false; + + + // ----------------------------------------------------------- Constructors + + /** + * No-args constructor + */ + public MultiThreadedHttpConnectionManager() { + this.connectionPool = new ConnectionPool(); + synchronized(ALL_CONNECTION_MANAGERS) { + ALL_CONNECTION_MANAGERS.put(this, null); + } + } + + + // ------------------------------------------------------- Instance Methods + + /** + * Shuts down the connection manager and releases all resources. All connections associated + * with this class will be closed and released. + * + *The connection manager can no longer be used once shutdown. + * + *
Calling this method more than once will have no effect.
+ */
+ public synchronized void shutdown() {
+ synchronized (connectionPool) {
+ if (!shutdown) {
+ shutdown = true;
+ connectionPool.shutdown();
+ }
+ }
+ }
+
+ /**
+ * @see HttpConnectionManager#getConnection(HostConfiguration)
+ */
+ public HttpHostConnection getConnection(HostConfiguration hostConfiguration) {
+
+ while (true) {
+ try {
+ return getConnection(hostConfiguration, 0);
+ } catch (ConnectionPoolTimeoutException e) {
+ // we'll go ahead and log this, but it should never happen. HttpExceptions
+ // are only thrown when the timeout occurs and since we have no timeout
+ // it should never happen.
+ LOG.debug(
+ "Unexpected exception while waiting for connection",
+ e
+ );
+ }
+ }
+ }
+
+ /**
+ * Gets a connection or waits if one is not available. A connection is
+ * available if one exists that is not being used or if fewer than
+ * maxHostConnections have been created in the connectionPool, and fewer
+ * than maxTotalConnections have been created in all connectionPools.
+ *
+ * @param hostConfiguration The host configuration specifying the connection
+ * details.
+ * @param timeout the number of milliseconds to wait for a connection, 0 to
+ * wait indefinitely
+ *
+ * @return HttpHostConnection an available connection
+ *
+ * @throws HttpException if a connection does not become available in
+ * 'timeout' milliseconds
+ *
+ * @since 3.0
+ */
+ public HttpHostConnection getConnection(HostConfiguration hostConfiguration,
+ long timeout) throws ConnectionPoolTimeoutException {
+
+ if (hostConfiguration == null) {
+ throw new IllegalArgumentException("hostConfiguration is null");
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("HttpConnectionManager.getConnection: config = "
+ + hostConfiguration + ", timeout = " + timeout);
+ }
+
+ final HttpHostConnection conn = doGetConnection(hostConfiguration, timeout);
+
+ // wrap the connection in an adapter so we can ensure it is used
+ // only once
+ return new HttpConnectionAdapter(conn);
+ }
+
+ private HttpHostConnection doGetConnection(HostConfiguration hostConfiguration,
+ long timeout) throws ConnectionPoolTimeoutException {
+
+ HttpHostConnection connection = null;
+
+ int maxHostConnections = HttpConnectionManagerParams
+ .getMaxConnectionsPerHost(this.params, hostConfiguration);
+ int maxTotalConnections = HttpConnectionManagerParams
+ .getMaxTotalConnections(this.params);
+
+ synchronized (connectionPool) {
+
+ // we clone the hostConfiguration
+ // so that it cannot be changed once the connection has been retrieved
+ hostConfiguration = new HostConfiguration(hostConfiguration);
+ HostConnectionPool hostPool = connectionPool.getHostPool(hostConfiguration);
+ WaitingThread waitingThread = null;
+
+ boolean useTimeout = (timeout > 0);
+ long timeToWait = timeout;
+ long startWait = 0;
+ long endWait = 0;
+
+ while (connection == null) {
+
+ if (shutdown) {
+ throw new IllegalStateException("Connection factory has been shutdown.");
+ }
+
+ // happen to have a free connection with the right specs
+ //
+ if (hostPool.freeConnections.size() > 0) {
+ connection = connectionPool.getFreeConnection(hostConfiguration);
+
+ // have room to make more
+ //
+ } else if ((hostPool.numConnections < maxHostConnections)
+ && (connectionPool.numConnections < maxTotalConnections)) {
+
+ connection = connectionPool.createConnection(hostConfiguration);
+
+ // have room to add host connection, and there is at least one free
+ // connection that can be liberated to make overall room
+ //
+ } else if ((hostPool.numConnections < maxHostConnections)
+ && (connectionPool.freeConnections.size() > 0)) {
+
+ connectionPool.deleteLeastUsedConnection();
+ connection = connectionPool.createConnection(hostConfiguration);
+
+ // otherwise, we have to wait for one of the above conditions to
+ // become true
+ //
+ } else {
+ // TODO: keep track of which hostConfigurations have waiting
+ // threads, so they avoid being sacrificed before necessary
+
+ try {
+
+ if (useTimeout && timeToWait <= 0) {
+ throw new ConnectionPoolTimeoutException("Timeout waiting for connection");
+ }
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Unable to get a connection, waiting..., hostConfig=" + hostConfiguration);
+ }
+
+ if (waitingThread == null) {
+ waitingThread = new WaitingThread();
+ waitingThread.hostConnectionPool = hostPool;
+ waitingThread.thread = Thread.currentThread();
+ }
+
+ if (useTimeout) {
+ startWait = System.currentTimeMillis();
+ }
+
+ hostPool.waitingThreads.addLast(waitingThread);
+ connectionPool.waitingThreads.addLast(waitingThread);
+ connectionPool.wait(timeToWait);
+
+ // we have not been interrupted so we need to remove ourselves from the
+ // wait queue
+ hostPool.waitingThreads.remove(waitingThread);
+ connectionPool.waitingThreads.remove(waitingThread);
+ } catch (InterruptedException e) {
+ // do nothing
+ } finally {
+ if (useTimeout) {
+ endWait = System.currentTimeMillis();
+ timeToWait -= (endWait - startWait);
+ }
+ }
+ }
+ }
+ }
+ return connection;
+ }
+
+ /**
+ * Gets the total number of pooled connections for the given host configuration. This
+ * is the total number of connections that have been created and are still in use
+ * by this connection manager for the host configuration. This value will
+ * not exceed the {@link #getMaxConnectionsPerHost() maximum number of connections per
+ * host}.
+ *
+ * @param hostConfiguration The host configuration
+ * @return The total number of pooled connections
+ */
+ public int getConnectionsInPool(HostConfiguration hostConfiguration) {
+ synchronized (connectionPool) {
+ HostConnectionPool hostPool = connectionPool.getHostPool(hostConfiguration);
+ return hostPool.numConnections;
+ }
+ }
+
+ /**
+ * Gets the total number of pooled connections. This is the total number of
+ * connections that have been created and are still in use by this connection
+ * manager. This value will not exceed the {@link #getMaxTotalConnections()
+ * maximum number of connections}.
+ *
+ * @return the total number of pooled connections
+ */
+ public int getConnectionsInPool() {
+ synchronized (connectionPool) {
+ return connectionPool.numConnections;
+ }
+ }
+
+ /**
+ * Deletes all closed connections. Only connections currently owned by the connection
+ * manager are processed.
+ *
+ * @see HttpHostConnection#isOpen()
+ *
+ * @since 3.0
+ */
+ public void deleteClosedConnections() {
+ connectionPool.deleteClosedConnections();
+ }
+
+ /**
+ * @since 3.0
+ */
+ public void closeIdleConnections(long idleTimeout) {
+ connectionPool.closeIdleConnections(idleTimeout);
+ deleteClosedConnections();
+ }
+
+ /**
+ * Make the given HttpHostConnection available for use by other requests.
+ * If another thread is blocked in getConnection() that could use this
+ * connection, it will be woken up.
+ *
+ * @param conn the HttpHostConnection to make available.
+ */
+ public void releaseConnection(HttpHostConnection conn) {
+
+ if (conn instanceof HttpConnectionAdapter) {
+ // connections given out are wrapped in an HttpConnectionAdapter
+ conn = ((HttpConnectionAdapter) conn).getWrappedConnection();
+ } else {
+ // this is okay, when an HttpConnectionAdapter is released
+ // is releases the real connection
+ }
+
+ // make sure that the response has been read.
+ SimpleHttpConnectionManager.finishLastResponse(conn);
+
+ connectionPool.freeConnection(conn);
+ }
+
+ /**
+ * Returns {@link HttpParams parameters} associated
+ * with this connection manager.
+ *
+ * @since 3.0
+ *
+ * @see HttpConnectionManagerParams
+ */
+ public HttpParams getParams() {
+ return this.params;
+ }
+
+ /**
+ * Assigns {@link HttpParams parameters} for this
+ * connection manager.
+ *
+ * @since 3.0
+ *
+ * @see HttpConnectionManagerParams
+ */
+ public void setParams(final HttpParams params) {
+ if (params == null) {
+ throw new IllegalArgumentException("Parameters may not be null");
+ }
+ this.params = params;
+ }
+
+ /**
+ * Global Connection Pool, including per-host pools
+ */
+ private class ConnectionPool {
+
+ /** The list of free connections */
+ private LinkedList freeConnections = new LinkedList();
+
+ /** The list of WaitingThreads waiting for a connection */
+ private LinkedList waitingThreads = new LinkedList();
+
+ /**
+ * Map where keys are {@link HostConfiguration}s and values are {@link
+ * HostConnectionPool}s
+ */
+ private final Map mapHosts = new HashMap();
+
+ private IdleConnectionHandler idleConnectionHandler = new IdleConnectionHandler();
+
+ /** The number of created connections */
+ private int numConnections = 0;
+
+ /**
+ * Cleans up all connection pool resources.
+ */
+ public synchronized void shutdown() {
+
+ // close all free connections
+ Iterator iter = freeConnections.iterator();
+ while (iter.hasNext()) {
+ HttpHostConnection conn = (HttpHostConnection) iter.next();
+ iter.remove();
+ closeConnection(conn);
+ }
+
+ // close all connections that have been checked out
+ shutdownCheckedOutConnections(this);
+
+ // interrupt all waiting threads
+ iter = waitingThreads.iterator();
+ while (iter.hasNext()) {
+ WaitingThread waiter = (WaitingThread) iter.next();
+ iter.remove();
+ waiter.thread.interrupt();
+ }
+
+ // clear out map hosts
+ mapHosts.clear();
+
+ // remove all references to connections
+ idleConnectionHandler.removeAll();
+ }
+
+ /**
+ * Creates a new connection and returns it for use of the calling method.
+ *
+ * @param hostConfiguration the configuration for the connection
+ * @return a new connection or null
if none are available
+ */
+ public synchronized HttpHostConnection createConnection(HostConfiguration hostConfiguration) {
+ HostConnectionPool hostPool = getHostPool(hostConfiguration);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Allocating new connection, hostConfig=" + hostConfiguration);
+ }
+ HttpConnectionWithReference connection = new HttpConnectionWithReference();
+ connection.setHttpConnectionManager(MultiThreadedHttpConnectionManager.this);
+ numConnections++;
+ hostPool.numConnections++;
+
+ // store a reference to this connection so that it can be cleaned up
+ // in the event it is not correctly released
+ storeReferenceToConnection(connection, hostConfiguration, this);
+ return connection;
+ }
+
+ /**
+ * Handles cleaning up for a lost connection with the given config. Decrements any
+ * connection counts and notifies waiting threads, if appropriate.
+ *
+ * @param config the host configuration of the connection that was lost
+ */
+ public synchronized void handleLostConnection(HostConfiguration config) {
+ HostConnectionPool hostPool = getHostPool(config);
+ hostPool.numConnections--;
+
+ numConnections--;
+ notifyWaitingThread(config);
+ }
+
+ /**
+ * Get the pool (list) of connections available for the given hostConfig.
+ *
+ * @param hostConfiguration the configuraton for the connection pool
+ * @return a pool (list) of connections available for the given config
+ */
+ public synchronized HostConnectionPool getHostPool(HostConfiguration hostConfiguration) {
+
+ // Look for a list of connections for the given config
+ HostConnectionPool listConnections = (HostConnectionPool)
+ mapHosts.get(hostConfiguration);
+ if (listConnections == null) {
+ // First time for this config
+ listConnections = new HostConnectionPool();
+ listConnections.hostConfiguration = hostConfiguration;
+ mapHosts.put(hostConfiguration, listConnections);
+ }
+
+ return listConnections;
+ }
+
+ /**
+ * If available, get a free connection for this host
+ *
+ * @param hostConfiguration the configuraton for the connection pool
+ * @return an available connection for the given config
+ */
+ public synchronized HttpHostConnection getFreeConnection(HostConfiguration hostConfiguration) {
+
+ HttpConnectionWithReference connection = null;
+
+ HostConnectionPool hostPool = getHostPool(hostConfiguration);
+
+ if (hostPool.freeConnections.size() > 0) {
+ connection = (HttpConnectionWithReference) hostPool.freeConnections.removeLast();
+ freeConnections.remove(connection);
+ // store a reference to this connection so that it can be cleaned up
+ // in the event it is not correctly released
+ storeReferenceToConnection(connection, hostConfiguration, this);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Getting free connection, hostConfig=" + hostConfiguration);
+ }
+
+ // remove the connection from the timeout handler
+ idleConnectionHandler.remove(connection);
+ } else if (LOG.isDebugEnabled()) {
+ LOG.debug("There were no free connections to get, hostConfig="
+ + hostConfiguration);
+ }
+ return connection;
+ }
+
+ /**
+ * Deletes all closed connections.
+ */
+ public synchronized void deleteClosedConnections() {
+
+ Iterator iter = freeConnections.iterator();
+
+ while (iter.hasNext()) {
+ HttpHostConnection conn = (HttpHostConnection) iter.next();
+ if (!conn.isOpen()) {
+ iter.remove();
+ deleteConnection(conn);
+ }
+ }
+ }
+
+ /**
+ * Closes idle connections.
+ * @param idleTimeout
+ */
+ public synchronized void closeIdleConnections(long idleTimeout) {
+ idleConnectionHandler.closeIdleConnections(idleTimeout);
+ }
+
+ /**
+ * Deletes the given connection. This will remove all reference to the connection
+ * so that it can be GCed.
+ *
+ *
Note: Does not remove the connection from the freeConnections list. It + * is assumed that the caller has already handled this case.
+ * + * @param connection The connection to delete + */ + private synchronized void deleteConnection(HttpHostConnection connection) { + + HostConfiguration connectionConfiguration = connection.getHostConfiguration(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Reclaiming connection, hostConfig=" + connectionConfiguration); + } + + closeConnection(connection); + + HostConnectionPool hostPool = getHostPool(connectionConfiguration); + + hostPool.freeConnections.remove(connection); + hostPool.numConnections--; + numConnections--; + + // remove the connection from the timeout handler + idleConnectionHandler.remove(connection); + } + + /** + * Close and delete an old, unused connection to make room for a new one. + */ + public synchronized void deleteLeastUsedConnection() { + + HttpHostConnection connection = (HttpHostConnection) freeConnections.removeFirst(); + + if (connection != null) { + deleteConnection(connection); + } else if (LOG.isDebugEnabled()) { + LOG.debug("Attempted to reclaim an unused connection but there were none."); + } + } + + /** + * Notifies a waiting thread that a connection for the given configuration is + * available. + * @param configuration the host config to use for notifying + * @see #notifyWaitingThread(HostConnectionPool) + */ + public synchronized void notifyWaitingThread(HostConfiguration configuration) { + notifyWaitingThread(getHostPool(configuration)); + } + + /** + * Notifies a waiting thread that a connection for the given configuration is + * available. This will wake a thread waiting in this host pool or if there is not + * one a thread in the connection pool will be notified. + * + * @param hostPool the host pool to use for notifying + */ + public synchronized void notifyWaitingThread(HostConnectionPool hostPool) { + + // find the thread we are going to notify, we want to ensure that each + // waiting thread is only interrupted once so we will remove it from + // all wait queues before interrupting it + WaitingThread waitingThread = null; + + if (hostPool.waitingThreads.size() > 0) { + if (LOG.isDebugEnabled()) { + LOG.debug("Notifying thread waiting on host pool, hostConfig=" + + hostPool.hostConfiguration); + } + waitingThread = (WaitingThread) hostPool.waitingThreads.removeFirst(); + waitingThreads.remove(waitingThread); + } else if (waitingThreads.size() > 0) { + if (LOG.isDebugEnabled()) { + LOG.debug("No-one waiting on host pool, notifying next waiting thread."); + } + waitingThread = (WaitingThread) waitingThreads.removeFirst(); + waitingThread.hostConnectionPool.waitingThreads.remove(waitingThread); + } else if (LOG.isDebugEnabled()) { + LOG.debug("Notifying no-one, there are no waiting threads"); + } + + if (waitingThread != null) { + waitingThread.thread.interrupt(); + } + } + + /** + * Marks the given connection as free. + * @param conn a connection that is no longer being used + */ + public void freeConnection(HttpHostConnection conn) { + + HostConfiguration connectionConfiguration = conn.getHostConfiguration(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Freeing connection, hostConfig=" + connectionConfiguration); + } + + synchronized (this) { + + if (shutdown) { + // the connection manager has been shutdown, release the connection's + // resources and get out of here + closeConnection(conn); + return; + } + + HostConnectionPool hostPool = getHostPool(connectionConfiguration); + + // Put the connect back in the available list and notify a waiter + hostPool.freeConnections.add(conn); + if (hostPool.numConnections == 0) { + // for some reason this connection pool didn't already exist + LOG.error("Host connection pool not found, hostConfig=" + + connectionConfiguration); + hostPool.numConnections = 1; + } + + freeConnections.add(conn); + // we can remove the reference to this connection as we have control over + // it again. this also ensures that the connection manager can be GCed + removeReferenceToConnection((HttpConnectionWithReference) conn); + if (numConnections == 0) { + // for some reason this connection pool didn't already exist + LOG.error("Host connection pool not found, hostConfig=" + + connectionConfiguration); + numConnections = 1; + } + + // register the connection with the timeout handler + idleConnectionHandler.add(conn); + + notifyWaitingThread(hostPool); + } + } + } + + private static void closeConnection(final HttpHostConnection conn) { + try { + conn.close(); + } catch (IOException ex) { + LOG.debug("I/O error closing connection", ex); + } + } + + /** + * A simple struct-like class to combine the objects needed to release a connection's + * resources when claimed by the garbage collector. + */ + private static class ConnectionSource { + + /** The connection pool that created the connection */ + public ConnectionPool connectionPool; + + /** The connection's host configuration */ + public HostConfiguration hostConfiguration; + } + + /** + * A simple struct-like class to combine the connection list and the count + * of created connections. + */ + private static class HostConnectionPool { + /** The hostConfig this pool is for */ + public HostConfiguration hostConfiguration; + + /** The list of free connections */ + public LinkedList freeConnections = new LinkedList(); + + /** The list of WaitingThreads for this host */ + public LinkedList waitingThreads = new LinkedList(); + + /** The number of created connections */ + public int numConnections = 0; + } + + /** + * A simple struct-like class to combine the waiting thread and the connection + * pool it is waiting on. + */ + private static class WaitingThread { + /** The thread that is waiting for a connection */ + public Thread thread; + + /** The connection pool the thread is waiting for */ + public HostConnectionPool hostConnectionPool; + } + + /** + * A thread for listening for HttpConnections reclaimed by the garbage + * collector. + */ + private static class ReferenceQueueThread extends Thread { + + private boolean shutdown = false; + + /** + * Create an instance and make this a daemon thread. + */ + public ReferenceQueueThread() { + setDaemon(true); + setName("MultiThreadedHttpConnectionManager cleanup"); + } + + public void shutdown() { + this.shutdown = true; + } + + /** + * Handles cleaning up for the given connection reference. + * + * @param ref the reference to clean up + */ + private void handleReference(Reference ref) { + + ConnectionSource source = null; + + synchronized (REFERENCE_TO_CONNECTION_SOURCE) { + source = (ConnectionSource) REFERENCE_TO_CONNECTION_SOURCE.remove(ref); + } + // only clean up for this reference if it is still associated with + // a ConnectionSource + if (source != null) { + if (LOG.isDebugEnabled()) { + LOG.debug( + "Connection reclaimed by garbage collector, hostConfig=" + + source.hostConfiguration); + } + + source.connectionPool.handleLostConnection(source.hostConfiguration); + } + } + + /** + * Start execution. + */ + public void run() { + while (!shutdown) { + try { + // remove the next reference and process it, a timeout + // is used so that the thread does not block indefinitely + // and therefore keep the thread from shutting down + Reference ref = REFERENCE_QUEUE.remove(1000); + if (ref != null) { + handleReference(ref); + } + } catch (InterruptedException e) { + LOG.debug("ReferenceQueueThread interrupted", e); + } + } + } + + } + + /** + * A connection that keeps a reference to itself. + */ + private static class HttpConnectionWithReference extends DefaultHttpHostConnection { + + public WeakReference reference = new WeakReference(this, REFERENCE_QUEUE); + + /** + * @param hostConfiguration + */ + public HttpConnectionWithReference() { + super(); + } + + } + + /** + * An HttpHostConnection wrapper that ensures a connection cannot be used + * once released. + */ + private static class HttpConnectionAdapter implements HttpHostConnection { + + /** the wrapped connection + */ + private HttpHostConnection wrappedConnection; + + /** + * Creates a new HttpConnectionAdapter. + * @param connection the connection to be wrapped + */ + public HttpConnectionAdapter(HttpHostConnection connection) { + super(); + this.wrappedConnection = connection; + } + + /** + * Tests if the wrapped connection is still available. + * @return boolean + */ + protected boolean hasConnection() { + return wrappedConnection != null; + } + + /** + * @return HttpHostConnection + */ + HttpHostConnection getWrappedConnection() { + return wrappedConnection; + } + + //======================= + // HttpConnection methods + //======================= + + public void close() throws IOException { + if (hasConnection()) { + wrappedConnection.close(); + } else { + // do nothing + } + } + + public boolean isOpen() { + if (hasConnection()) { + return wrappedConnection.isOpen(); + } else { + return false; + } + } + + public boolean isStale() { + if (hasConnection()) { + return wrappedConnection.isStale(); + } else { + return true; + } + } + + public void shutdown() throws IOException { + if (hasConnection()) { + wrappedConnection.shutdown(); + } else { + // do nothing + } + } + + //============================= + // HttpClientConnection methods + //============================= + public void flush() throws IOException { + if (hasConnection()) { + wrappedConnection.flush(); + } else { + // do nothing + } + } + + public boolean isResponseAvailable(int timeout) throws IOException { + if (hasConnection()) { + return wrappedConnection.isResponseAvailable(timeout); + } else { + return false; + } + } + + public HttpResponse receiveResponseHeader(final HttpParams params) + throws HttpException, IOException { + if (hasConnection()) { + return wrappedConnection.receiveResponseHeader(params); + } else { + return null; + } + } + + public void receiveResponseEntity(final HttpResponse response) + throws HttpException, IOException { + if (hasConnection()) { + wrappedConnection.receiveResponseEntity(response); + } else { + // do nothing + } + } + + public void sendRequestHeader(final HttpRequest request) + throws HttpException, IOException { + if (hasConnection()) { + wrappedConnection.sendRequestHeader(request); + } else { + // do nothing + } + } + + public void sendRequestEntity(final HttpEntityEnclosingRequest request) + throws HttpException, IOException { + if (hasConnection()) { + wrappedConnection.sendRequestEntity(request); + } else { + // do nothing + } + } + + //=========================== + // HttpInetConnection methods + //=========================== + public InetAddress getLocalAddress() { + if (hasConnection()) { + return wrappedConnection.getLocalAddress(); + } else { + return null; + } + } + + public InetAddress getRemoteAddress() { + if (hasConnection()) { + return wrappedConnection.getRemoteAddress(); + } else { + return null; + } + } + + //=========================== + // HttpHostConnection methods + //=========================== + public void setHttpConnectionManager(final HttpConnectionManager httpConnectionManager) { + if (hasConnection()) { + wrappedConnection.setHttpConnectionManager(httpConnectionManager); + } else { + // do nothing + } + } + + public boolean isLocked() { + if (hasConnection()) { + return wrappedConnection.isLocked(); + } else { + return false; + } + } + + public void setLocked(boolean locked) { + if (hasConnection()) { + wrappedConnection.setLocked(locked); + } else { + // do nothing + } + } + + public void releaseConnection() { + HttpHostConnection conn = this.wrappedConnection; + if (conn != null && !conn.isLocked()) { + this.wrappedConnection = null; + conn.releaseConnection(); + } else { + // do nothing + } + } + + public HostConfiguration getHostConfiguration() { + if (hasConnection()) { + return wrappedConnection.getHostConfiguration(); + } else { + return null; + } + } + + public void open(final HttpParams params) throws IOException { + if (hasConnection()) { + wrappedConnection.open(params); + } else { + // do nothing + } + } + + public void tunnelCreated(final HttpParams params) throws IOException { + if (hasConnection()) { + wrappedConnection.tunnelCreated(params); + } else { + // do nothing + } + } + + public void setSocketTimeout(int timeout) throws SocketException { + if (hasConnection()) { + wrappedConnection.setSocketTimeout(timeout); + } else { + // do nothing + } + } + + public HttpResponse getLastResponse() { + if (hasConnection()) { + return wrappedConnection.getLastResponse(); + } else { + return null; + } + } + + public void setLastResponse(final HttpResponse response) { + if (hasConnection()) { + wrappedConnection.setLastResponse(response); + } else { + // do nothing + } + } + + } + +} + diff --git a/src/java/org/apache/http/conn/impl/SimpleHttpConnectionManager.java b/src/java/org/apache/http/conn/impl/SimpleHttpConnectionManager.java index d2f573116..bf18824d5 100644 --- a/src/java/org/apache/http/conn/impl/SimpleHttpConnectionManager.java +++ b/src/java/org/apache/http/conn/impl/SimpleHttpConnectionManager.java @@ -69,7 +69,7 @@ public class SimpleHttpConnectionManager implements HttpConnectionManager { * consume it now. * @param conn The connection */ - private void finishLastResponse(DefaultHttpHostConnection conn) { + protected static void finishLastResponse(HttpHostConnection conn) { HttpResponse lastResponse = conn.getLastResponse(); if (lastResponse != null) { conn.setLastResponse(null); @@ -158,7 +158,8 @@ public class SimpleHttpConnectionManager implements HttpConnectionManager { HostConfiguration hostConfiguration, long timeout) { if (httpConnection == null) { - httpConnection = new DefaultHttpHostConnection(this); + httpConnection = new DefaultHttpHostConnection(); + httpConnection.setHttpConnectionManager(this); } else { // make sure the host and proxy are correct for this connection // close it and set the values if they are not diff --git a/src/java/org/apache/http/conn/params/HttpConnectionManagerParams.java b/src/java/org/apache/http/conn/params/HttpConnectionManagerParams.java new file mode 100644 index 000000000..147088617 --- /dev/null +++ b/src/java/org/apache/http/conn/params/HttpConnectionManagerParams.java @@ -0,0 +1,222 @@ +/* + * $HeadURL$ + * $Revision$ + * $Date$ + * + * ==================================================================== + * + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + *+ * This parameter expects a value of type {@link java.util.Map}. The value + * should map instances of {@link HostConfiguration} + * to {@link Integer integers}. The default value can be specified using + * {@link HostConfiguration#ANY_HOST_CONFIGURATION}. + *
+ */ + public static final String MAX_HOST_CONNECTIONS = "http.connection-manager.max-per-host"; + + /** + * Defines the maximum number of connections allowed overall. This value only applies + * to the number of connections from a particular instance of HttpConnectionManager. + *+ * This parameter expects a value of type {@link Integer}. + *
+ */ + public static final String MAX_TOTAL_CONNECTIONS = "http.connection-manager.max-total"; + + /** + * Sets the default maximum number of connections allowed for a given + * host config. + * + * @param maxHostConnections The default maximum. + * + * @see #MAX_HOST_CONNECTIONS + */ + public static void setDefaultMaxConnectionsPerHost( + final HttpParams params, + int maxHostConnections) { + setMaxConnectionsPerHost( + params, + HostConfiguration.ANY_HOST_CONFIGURATION, + maxHostConnections); + } + + /** + * Sets the maximum number of connections to be used for the given host config. + * + * @param hostConfiguration The host config to set the maximum for. Use + * {@link HostConfiguration#ANY_HOST_CONFIGURATION} to configure the default value + * per host. + * @param maxHostConnections The maximum number of connections,> 0
+ *
+ * @see #MAX_HOST_CONNECTIONS
+ */
+ public static void setMaxConnectionsPerHost(
+ final HttpParams params,
+ final HostConfiguration hostConfiguration,
+ int maxHostConnections) {
+
+ if (params == null) {
+ throw new IllegalArgumentException("HTTP parameters may not be null");
+ }
+ if (maxHostConnections <= 0) {
+ throw new IllegalArgumentException("maxHostConnections must be greater than 0");
+ }
+
+ Map currentValues = (Map) params.getParameter(MAX_HOST_CONNECTIONS);
+ // param values are meant to be immutable so we'll make a copy
+ // to modify
+ Map newValues = null;
+ if (currentValues == null) {
+ newValues = new HashMap();
+ } else {
+ newValues = new HashMap(currentValues);
+ }
+ newValues.put(hostConfiguration, new Integer(maxHostConnections));
+ params.setParameter(MAX_HOST_CONNECTIONS, newValues);
+ }
+
+ /**
+ * Gets the default maximum number of connections allowed for a given
+ * host config.
+ *
+ * @return The default maximum.
+ *
+ * @see #MAX_HOST_CONNECTIONS
+ */
+ public static int getDefaultMaxConnectionsPerHost(
+ final HttpParams params) {
+ return getMaxConnectionsPerHost(
+ params,
+ HostConfiguration.ANY_HOST_CONFIGURATION);
+ }
+
+ /**
+ * Gets the maximum number of connections to be used for a particular host config. If
+ * the value has not been specified for the given host the default value will be
+ * returned.
+ *
+ * @param hostConfiguration The host config.
+ * @return The maximum number of connections to be used for the given host config.
+ *
+ * @see #MAX_HOST_CONNECTIONS
+ */
+ public static int getMaxConnectionsPerHost(
+ final HttpParams params,
+ final HostConfiguration hostConfiguration) {
+
+ if (params == null) {
+ throw new IllegalArgumentException("HTTP parameters may not be null");
+ }
+ Map m = (Map) params.getParameter(MAX_HOST_CONNECTIONS);
+ if (m == null) {
+ // MAX_HOST_CONNECTIONS have not been configured, using the default value
+ return DEFAULT_MAX_HOST_CONNECTIONS;
+ } else {
+ Integer max = (Integer) m.get(hostConfiguration);
+ if (max == null && hostConfiguration != HostConfiguration.ANY_HOST_CONFIGURATION) {
+ // the value has not been configured specifically for this host config,
+ // use the default value
+ return getMaxConnectionsPerHost(params, HostConfiguration.ANY_HOST_CONFIGURATION);
+ } else {
+ return (
+ max == null
+ ? DEFAULT_MAX_HOST_CONNECTIONS
+ : max.intValue()
+ );
+ }
+ }
+ }
+
+ /**
+ * Sets the maximum number of connections allowed.
+ *
+ * @param maxTotalConnections The maximum number of connections allowed.
+ *
+ * @see #MAX_TOTAL_CONNECTIONS
+ */
+ public static void setMaxTotalConnections(
+ final HttpParams params,
+ int maxTotalConnections) {
+ if (params == null) {
+ throw new IllegalArgumentException("HTTP parameters may not be null");
+ }
+ params.setIntParameter(
+ HttpConnectionManagerParams.MAX_TOTAL_CONNECTIONS,
+ maxTotalConnections);
+ }
+
+ /**
+ * Gets the maximum number of connections allowed.
+ *
+ * @return The maximum number of connections allowed.
+ *
+ * @see #MAX_TOTAL_CONNECTIONS
+ */
+ public static int getMaxTotalConnections(
+ final HttpParams params) {
+ if (params == null) {
+ throw new IllegalArgumentException("HTTP parameters may not be null");
+ }
+ return params.getIntParameter(
+ HttpConnectionManagerParams.MAX_TOTAL_CONNECTIONS,
+ DEFAULT_MAX_TOTAL_CONNECTIONS);
+ }
+
+}