HTTPCLIENT-1583: optional connection eviction thread for non-shared connection managers to pro-actively evict expired / idle persistent connections from the connection pool

git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1643671 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2014-12-07 12:42:19 +00:00
parent 602d9f9fc9
commit 5a192e2bf4
5 changed files with 288 additions and 84 deletions

View File

@ -30,10 +30,10 @@ import java.util.concurrent.TimeUnit;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.pool.PoolStats;
import org.apache.http.util.EntityUtils;
/**
@ -47,6 +47,8 @@ public class ClientEvictExpiredConnections {
cm.setMaxTotal(100);
CloseableHttpClient httpclient = HttpClients.custom()
.setConnectionManager(cm)
.evictExpiredConnections()
.evictIdleConnections(5L, TimeUnit.SECONDS)
.build();
try {
// create an array of URIs to perform GETs on
@ -56,9 +58,6 @@ public class ClientEvictExpiredConnections {
"http://hc.apache.org/httpcomponents-client-ga/",
};
IdleConnectionEvictor connEvictor = new IdleConnectionEvictor(cm);
connEvictor.start();
for (int i = 0; i < urisToGet.length; i++) {
String requestURI = urisToGet[i];
HttpGet request = new HttpGet(requestURI);
@ -75,54 +74,18 @@ public class ClientEvictExpiredConnections {
}
}
// Sleep 10 sec and let the connection evictor do its job
Thread.sleep(20000);
PoolStats stats1 = cm.getTotalStats();
System.out.println("Connections kept alive: " + stats1.getAvailable());
// Shut down the evictor thread
connEvictor.shutdown();
connEvictor.join();
// Sleep 10 sec and let the connection evictor do its job
Thread.sleep(10000);
PoolStats stats2 = cm.getTotalStats();
System.out.println("Connections kept alive: " + stats2.getAvailable());
} finally {
httpclient.close();
}
}
public static class IdleConnectionEvictor extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionEvictor(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(5000);
// Close expired connections
connMgr.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 5 sec
connMgr.closeIdleConnections(5, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
}

View File

@ -197,6 +197,10 @@ public class HttpClientBuilder {
private SocketConfig defaultSocketConfig;
private ConnectionConfig defaultConnectionConfig;
private RequestConfig defaultRequestConfig;
private boolean evictExpiredConnections;
private boolean evictIdleConnections;
private long maxIdleTime;
private TimeUnit maxIdleTimeUnit;
private boolean systemProperties;
private boolean redirectHandlingDisabled;
@ -749,7 +753,60 @@ public class HttpClientBuilder {
* implementations.
*/
public final HttpClientBuilder useSystemProperties() {
systemProperties = true;
this.systemProperties = true;
return this;
}
/**
* Makes this instance of HttpClient proactively evict expired connections from the
* connection pool using a background thread.
* <p>
* One MUST explicitly close HttpClient with {@link CloseableHttpClient#close()} in order
* to stop and release the background thread.
* <p>
* Please note this method has no effect if the instance of HttpClient is configuted to
* use a shared connection manager.
* <p>
* Please note this method may not be used when the instance of HttpClient is created
* inside an EJB container.
*
* @see #setConnectionManagerShared(boolean)
* @see org.apache.http.conn.HttpClientConnectionManager#closeExpiredConnections()
*
* @since 4.4
*/
public final HttpClientBuilder evictExpiredConnections() {
evictExpiredConnections = true;
return this;
}
/**
* Makes this instance of HttpClient proactively evict idle connections from the
* connection pool using a background thread.
* <p>
* One MUST explicitly close HttpClient with {@link CloseableHttpClient#close()} in order
* to stop and release the background thread.
* <p>
* Please note this method has no effect if the instance of HttpClient is configuted to
* use a shared connection manager.
* <p>
* Please note this method may not be used when the instance of HttpClient is created
* inside an EJB container.
*
* @see #setConnectionManagerShared(boolean)
* @see org.apache.http.conn.HttpClientConnectionManager#closeExpiredConnections()
*
* @param maxIdleTime maxium time persistent connections can stay idle while kept alive
* in the connection pool. Connections whose inactivity period exceeds this value will
* get closed and evicted from the pool.
* @param maxIdleTimeUnit time unit for the above parameter.
*
* @since 4.4
*/
public final HttpClientBuilder evictIdleConnections(final Long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this.evictIdleConnections = true;
this.maxIdleTime = maxIdleTime;
this.maxIdleTimeUnit = maxIdleTimeUnit;
return this;
}
@ -1098,6 +1155,20 @@ public class HttpClientBuilder {
closeablesCopy = new ArrayList<Closeable>(1);
}
final HttpClientConnectionManager cm = connManagerCopy;
if (evictExpiredConnections || evictIdleConnections) {
final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS);
closeablesCopy.add(new Closeable() {
@Override
public void close() throws IOException {
connectionEvictor.shutdown();
}
});
connectionEvictor.start();
}
closeablesCopy.add(new Closeable() {
@Override

View File

@ -0,0 +1,123 @@
/*
* ====================================================================
* 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
* <http://www.apache.org/>.
*
*/
package org.apache.http.impl.client;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.util.Args;
/**
* This class maintains a background thread to enforce an eviction policy for expired / idle
* persistent connections kept alive in the connection pool.
*
* @since 4.4
*/
public final class IdleConnectionEvictor {
private final HttpClientConnectionManager connectionManager;
private final ThreadFactory threadFactory;
private final Thread thread;
private final long sleepTimeMs;
private final long maxIdleTimeMs;
private volatile Exception exception;
public IdleConnectionEvictor(
final HttpClientConnectionManager connectionManager,
final ThreadFactory threadFactory,
final long sleepTime, final TimeUnit sleepTimeUnit,
final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this.connectionManager = Args.notNull(connectionManager, "Connection manager");
this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
this.thread = this.threadFactory.newThread(new Runnable() {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(sleepTimeMs);
connectionManager.closeExpiredConnections();
if (maxIdleTimeMs > 0) {
connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
}
}
} catch (Exception ex) {
exception = ex;
}
}
});
}
public IdleConnectionEvictor(
final HttpClientConnectionManager connectionManager,
final long sleepTime, final TimeUnit sleepTimeUnit,
final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this(connectionManager, null, sleepTime, sleepTimeUnit, maxIdleTime, maxIdleTimeUnit);
}
public IdleConnectionEvictor(
final HttpClientConnectionManager connectionManager,
final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this(connectionManager, null,
maxIdleTime > 0 ? maxIdleTime : 5, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,
maxIdleTime, maxIdleTimeUnit);
}
public void start() {
thread.start();
}
public void shutdown() {
thread.interrupt();
}
public boolean isRunning() {
return thread.isAlive();
}
public void awaitTermination(final long time, final TimeUnit tunit) throws InterruptedException {
thread.join((tunit != null ? tunit : TimeUnit.MILLISECONDS).toMillis(time));
}
static class DefaultThreadFactory implements ThreadFactory {
@Override
public Thread newThread(final Runnable r) {
final Thread t = new Thread(r, "Connection evictor");
t.setDaemon(true);
return t;
}
};
}

View File

@ -0,0 +1,80 @@
/*
* ====================================================================
* 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
* <http://www.apache.org/>.
*
*/
package org.apache.http.impl.client;
import java.util.concurrent.TimeUnit;
import org.apache.http.conn.HttpClientConnectionManager;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
/**
* Unit tests for {@link org.apache.http.impl.client.IdleConnectionEvictor}.
*/
public class TestIdleConnectionEvictor {
@Test
public void testEvictExpiredAndIdle() throws Exception {
final HttpClientConnectionManager cm = Mockito.mock(HttpClientConnectionManager.class);
final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
500, TimeUnit.MILLISECONDS, 3, TimeUnit.SECONDS);
connectionEvictor.start();
Thread.sleep(1000);
Mockito.verify(cm, Mockito.atLeast(1)).closeExpiredConnections();
Mockito.verify(cm, Mockito.atLeast(1)).closeIdleConnections(3000, TimeUnit.MILLISECONDS);
Assert.assertTrue(connectionEvictor.isRunning());
connectionEvictor.shutdown();
connectionEvictor.awaitTermination(1, TimeUnit.SECONDS);
Assert.assertFalse(connectionEvictor.isRunning());
}
@Test
public void testEvictExpiredOnly() throws Exception {
final HttpClientConnectionManager cm = Mockito.mock(HttpClientConnectionManager.class);
final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
500, TimeUnit.MILLISECONDS, 0, TimeUnit.SECONDS);
connectionEvictor.start();
Thread.sleep(1000);
Mockito.verify(cm, Mockito.atLeast(1)).closeExpiredConnections();
Mockito.verify(cm, Mockito.never()).closeIdleConnections(Mockito.anyLong(), Mockito.<TimeUnit>any());
Assert.assertTrue(connectionEvictor.isRunning());
connectionEvictor.shutdown();
connectionEvictor.awaitTermination(1, TimeUnit.SECONDS);
Assert.assertFalse(connectionEvictor.isRunning());
}
}

View File

@ -35,7 +35,7 @@ import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.IdleConnectionEvictor;
import org.apache.http.localserver.LocalServerTestBase;
import org.apache.http.util.EntityUtils;
import org.junit.Test;
@ -49,7 +49,8 @@ public class TestIdleConnectionEviction extends LocalServerTestBase {
final HttpHost target = start();
final IdleConnectionMonitor idleConnectionMonitor = new IdleConnectionMonitor(this.connManager);
final IdleConnectionEvictor idleConnectionMonitor = new IdleConnectionEvictor(
this.connManager, 50, TimeUnit.MILLISECONDS);
idleConnectionMonitor.start();
final HttpGet httpget = new HttpGet("/random/1024");
@ -115,38 +116,4 @@ public class TestIdleConnectionEviction extends LocalServerTestBase {
}
public static class IdleConnectionMonitor extends Thread {
private final HttpClientConnectionManager cm;
private volatile boolean shutdown;
public IdleConnectionMonitor(final HttpClientConnectionManager cm) {
super();
this.cm = cm;
setDaemon(true);
}
@Override
public void run() {
try {
while (!this.shutdown) {
synchronized (this) {
wait(250);
this.cm.closeIdleConnections(1, TimeUnit.MILLISECONDS);
}
}
} catch (final InterruptedException ex) {
// terminate
}
}
public void shutdown() {
this.shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
}