
io.jsync.http.impl.PriorityHttpConnectionPool Maven / Gradle / Ivy
Show all versions of jsync.io Show documentation
/*
* Copyright (c) 2011-2013 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.jsync.http.impl;
import io.jsync.Handler;
import io.jsync.impl.ConcurrentHashSet;
import io.jsync.impl.DefaultContext;
import io.jsync.logging.Logger;
import io.jsync.logging.impl.LoggerFactory;
import java.util.*;
/**
* @author Nathan Pahucki
* @author Tim Fox
*/
public abstract class PriorityHttpConnectionPool implements HttpPool {
private static final Logger log = LoggerFactory.getLogger(PriorityHttpConnectionPool.class);
private final Set available = new HashSet<>();
private final Set allConnections = new ConcurrentHashSet<>();
private final Queue waiters = new ArrayDeque<>();
private int maxPoolSize = 1;
private int connectionCount;
private int maxWaiterQueueSize = -1;
/**
* Returns the maximum number of connections in the pool
*/
public int getMaxPoolSize() {
return maxPoolSize;
}
/**
* Set the maximum pool size to the value specified by {@code maxConnections}
* The client will maintain up to {@code maxConnections} HTTP connections in an internal pool
*/
public void setMaxPoolSize(int maxConnections) {
this.maxPoolSize = maxConnections;
}
public int getMaxWaiterQueueSize() {
return maxWaiterQueueSize;
}
public void setMaxWaiterQueueSize(final int maxWaiterQueueSize) {
this.maxWaiterQueueSize = maxWaiterQueueSize;
}
public synchronized void report() {
log.trace("available: " + available.size() + " connection count: " + connectionCount + " waiters: " + waiters.size());
}
public void getConnection(final Handler handler, Handler connectExceptionHandler,
DefaultContext context) {
boolean connect = false;
ClientConnection conn;
outer:
synchronized (this) {
conn = selectConnection(available, connectionCount, maxPoolSize);
if (conn != null) {
break outer;
} else {
if (connectionCount < maxPoolSize) {
//Create new connection
connect = true;
connectionCount++;
break outer;
}
// Add to waiters
if (maxWaiterQueueSize == -1 || maxWaiterQueueSize > waiters.size()) {
waiters.add(new Waiter(handler, connectExceptionHandler, context));
} else {
// There are too many requests in waiter queue. Return exception to avoid OOM.
connectExceptionHandler.handle(new ConnectionPoolTooBusyException("Too many requests to be handled. The request will be cancelled to avoid OOM."));
}
}
}
// We do this outside the sync block to minimise the critical section
if (conn != null) {
handler.handle(conn);
} else if (connect) {
connect(new Handler() {
@Override
public void handle(ClientConnection conn) {
allConnections.add(conn);
handler.handle(conn);
}
}, connectExceptionHandler, context);
}
}
/**
* Inform the pool that the connection has been closed externally
* or the connection attempt failed
*/
public void connectionClosed(ClientConnection conn) {
Waiter waiter;
synchronized (this) {
connectionCount--;
if (conn != null) {
allConnections.remove(conn);
available.remove(conn);
}
if (connectionCount < maxPoolSize) {
//Now the connection count has come down, maybe there is another waiter that can
//create a new connection
waiter = waiters.poll();
if (waiter != null) {
connectionCount++;
}
} else {
waiter = null;
}
}
// We do the actual connect outside the sync block to minimise the critical section
if (waiter != null) {
connect(waiter.handler, waiter.connectionExceptionHandler, waiter.context);
}
}
/**
* Return a connection to the pool so it can be used by others.
*/
public void returnConnection(final ClientConnection conn) {
Waiter waiter;
synchronized (this) {
//Return it to the pool
waiter = waiters.poll();
if (waiter == null) {
available.add(conn);
}
}
if (waiter != null) {
final Waiter w = waiter;
w.context.execute(new Runnable() {
public void run() {
w.handler.handle(conn);
}
});
}
}
/**
* Close the pool
*/
public void close() {
synchronized (this) {
available.clear();
waiters.clear();
}
for (ClientConnection conn : allConnections) {
try {
conn.actualClose();
} catch (Throwable t) {
log.error("Failed to close connection", t);
}
}
allConnections.clear();
}
/**
* Implement this method in a sub-class to implement the actual connection creation for the specific type of connection
*/
protected abstract void connect(final Handler connectHandler, final Handler connectErrorHandler, final DefaultContext context);
private ClientConnection selectConnection(Set available, int connectionCount, int maxPoolSize) {
ClientConnection conn = null;
if (!available.isEmpty()) {
final boolean useOccupiedConnections = connectionCount >= maxPoolSize;
final Iterator clientConnectionIterator = available.iterator();
while (clientConnectionIterator.hasNext()) {
final ClientConnection c = clientConnectionIterator.next();
if (c.isClosed()) {
// remove closed connections from pool
clientConnectionIterator.remove();
continue;
}
// Ideal situation for all cases, a cached but unoccupied connection.
if (c.getOutstandingRequestCount() < 1) {
conn = c;
break;
}
// prevent a fully occupied from picking more requests since in this case the new incoming requests will probably time out.
if (c.isFullyOccupied()) {
clientConnectionIterator.remove();
continue;
}
if (useOccupiedConnections) {
// Otherwise, lets try to pick the connection that has the least amount of outstanding requests on it,
// even though we don't have any good way to know how long the requests in the front of this one might take
// it's still better than the old behavior which seems to glob all the requests into the first connection
// in the available list.
if (conn == null || conn.getOutstandingRequestCount() > c.getOutstandingRequestCount()) {
conn = c;
}
}
}
if (conn != null) available.remove(conn);
}
return conn; // might still be null, which would either create a connection, or put the request in a wait list
}
private static class Waiter {
final Handler handler;
final Handler connectionExceptionHandler;
final DefaultContext context;
private Waiter(Handler handler, Handler connectionExceptionHandler, DefaultContext context) {
this.handler = handler;
this.connectionExceptionHandler = connectionExceptionHandler;
this.context = context;
}
}
}