package org.limewire.net; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.HashMap; import java.util.Map; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.net.ProxyManager.ProxyConnector; import org.limewire.nio.NBSocket; import org.limewire.nio.NBSocketFactory; import org.limewire.nio.observer.ConnectObserver; import org.limewire.nio.observer.Shutdownable; import org.limewire.inspection.Inspectable; import org.limewire.inspection.InspectionPoint; import org.limewire.inspection.InspectionHistogram; import org.limewire.inspection.InspectionRequirement; import com.google.inject.Inject; import com.google.inject.Singleton; /** * A SocketController that does everything SimpleSocketController does, * except limits the number of outgoing attempts at any given moment. * * Connection attempts beyond the limit will be queued until space * is available for connecting. */ @Singleton class LimitedSocketController extends AbstractSocketController { private final static Log LOG = LogFactory.getLog(LimitedSocketController.class); private static final int DEFAULT_MAX_CONNECTING_SOCKETS = 4; /** * The maximum number of concurrent connection attempts. */ private final int MAX_CONNECTING_SOCKETS; /** * The current number of waiting socket attempts. */ private int _socketsConnecting = 0; /** * Any non-blocking Requestors waiting on a pending socket. */ private final List<Requestor> WAITING_REQUESTS = new LinkedList<Requestor>(); /** * Inspections related to the queue of connection attempt requestors: * * 1. Maximum number of requests in the queue * 2. Num of requests in queue cancelled before conn attempted * 3. Total requests that have been gone thru the queue * 4. Average time spent waiting in the queue * 5. Maximum time 1 request has spent in the queue * */ @InspectionPoint(value = "limited-socket-stats", requires = InspectionRequirement.OS_WINDOWS) private final LimitedSocketInspectable inspectable = new LimitedSocketInspectable(); /** * A histogram representing the number of requests in the waiting queue upon connection attempt * (inspection gathered at beginning of call to {@link #connectPlain}) */ @InspectionPoint(value = "limited-socket-req", requires = InspectionRequirement.OS_WINDOWS) private final InspectionHistogram<Integer> requestsInQueue = new InspectionHistogram<Integer>(); /** * Constructs a new LimitedSocketController that only allows 'max' * number of connections concurrently. */ @Inject LimitedSocketController(ProxyManager proxyManager, SocketBindingSettings socketBindingSettings) { this(proxyManager, socketBindingSettings, DEFAULT_MAX_CONNECTING_SOCKETS); } LimitedSocketController(ProxyManager proxyManager, SocketBindingSettings socketBindingSettings, int maxConnectingSockets) { super(proxyManager, socketBindingSettings); this.MAX_CONNECTING_SOCKETS = maxConnectingSockets; } /** * Connects to the given InetSocketAddress. * This will only connect if the number of connecting sockets has not * exceeded it's limit. If we're above the limit already, then * the connection attempt will not take place until a prior attempt * completes (either by success or failure). * If observer is null, this will block until this connection attempt finishes. * Otherwise, observer will be notified of success or failure. */ @Override protected Socket connectPlain(InetSocketAddress localAddr, NBSocketFactory factory, InetSocketAddress addr, int timeout, ConnectObserver observer) throws IOException { NBSocket socket = factory.createSocket(); bindSocket(socket, localAddr); requestsInQueue.count(getNumWaitingSockets()); if(observer == null) { if(LOG.isDebugEnabled()) { int waiting = getNumWaitingSockets(); LOG.debug(waiting + " waiting for sockets (blocking)"); } // BLOCKING. waitForSocket(); if(LOG.isDebugEnabled()) { String ipp = addr.getAddress().getHostAddress() + ":" + addr.getPort(); LOG.debug("Connecting to " + ipp + " (blocking)"); } try { socket.connect(addr, timeout); } finally { releaseSocket(); } } else { // NON BLOCKING if(addWaitingSocket(socket, addr, timeout, observer)) { if(LOG.isDebugEnabled()) { String ipp = addr.getAddress().getHostAddress() + ":" + addr.getPort(); LOG.debug("Connecting to " + ipp + " (non-blocking)"); } socket.connect(addr, timeout, new DelegateConnector(observer)); } else { if(LOG.isDebugEnabled()) { int waiting = getNumWaitingSockets(); LOG.debug(waiting + " waiting for sockets (non-blocking)"); } } } return socket; } /** * Removes the given observer from connecting. If the attempt has already begun, * this will return false and the observer will eventually be notified. * Otherwise, this will return true and the observer will never be notified, * because the connection will never be attempted. */ @Override public synchronized boolean removeConnectObserver(ConnectObserver observer) { for(Iterator<Requestor> i = WAITING_REQUESTS.iterator(); i.hasNext(); ) { Requestor next = i.next(); if(next.observer == observer) { i.remove(); return true; // must handle proxy'd kinds also. } else if(next.observer instanceof ProxyConnector) { if(((ProxyConnector)next.observer).getDelegateObserver() == observer) { i.remove(); return true; } } } return false; } /** Returns the maximum number of concurrent attempts this will allow. */ @Override public int getNumAllowedSockets() { return MAX_CONNECTING_SOCKETS; } /** Returns the number of sockets waiting. */ @Override public synchronized int getNumWaitingSockets() { return WAITING_REQUESTS.size(); } /** * Runs through any waiting Requestors and initiates a connection to them. */ private void runWaitingRequests() { // We must connect outside of the lock, so as not to expose being locked to external // entities. List<Requestor> toBeProcessed = new ArrayList<Requestor>(Math.min(WAITING_REQUESTS.size(), Math.max(0, MAX_CONNECTING_SOCKETS - _socketsConnecting))); synchronized(this) { while(_socketsConnecting < MAX_CONNECTING_SOCKETS && !WAITING_REQUESTS.isEmpty()) { Requestor next = WAITING_REQUESTS.remove(0); if(!next.socket.isClosed()) { toBeProcessed.add(next); _socketsConnecting++; } else { inspectable.incrementCancelledRequestCount(); } } } for(int i = 0; i < toBeProcessed.size(); i++) { Requestor next = toBeProcessed.get(i); inspectable.addReadyRequest(next); if(LOG.isDebugEnabled()) { String ipp = next.addr.getAddress().getHostAddress() + ":" + next.addr.getPort(); LOG.debug("Connecting to " + ipp + " (waiting)"); } next.socket.setShutdownObserver(null); next.socket.connect(next.addr, next.timeout, new DelegateConnector(next.observer)); } } /** * Determines if the given requestor can immediately connect. * If not, adds it to a pool of future connection-wanters. */ private synchronized boolean addWaitingSocket(NBSocket socket, InetSocketAddress addr, int timeout, ConnectObserver observer) { if (_socketsConnecting >= MAX_CONNECTING_SOCKETS) { WAITING_REQUESTS.add(new Requestor(socket, addr, timeout, observer)); inspectable.setMaxConnReqInQueueIfNecessary(getNumWaitingSockets()); socket.setShutdownObserver(new RemovalObserver(observer)); return false; } else { _socketsConnecting++; return true; } } /** * Waits until we're allowed to do an active outgoing socket connection. */ private synchronized void waitForSocket() throws IOException { while (_socketsConnecting >= MAX_CONNECTING_SOCKETS) { try { wait(); } catch (InterruptedException ix) { throw new IOException(ix.getMessage()); } } _socketsConnecting++; } /** * Notification that a socket has been released. * * If there are waiting non-blocking requests, spawns starts a new connection for them. */ private void releaseSocket() { // Release this slot. synchronized(this) { _socketsConnecting--; if(LOG.isDebugEnabled()) { LOG.debug("Releasing socket, " + _socketsConnecting + " connecting, " + getNumWaitingSockets() + " waiting"); } } // See if any non-blocking requests are queued. runWaitingRequests(); // If there's room, notify blocking requests. synchronized(this) { if(_socketsConnecting < MAX_CONNECTING_SOCKETS) { notifyAll(); } } } /** * An observer that is notified if the socket is shutdown while it is * in the requesting list. */ private class RemovalObserver implements Shutdownable { private final ConnectObserver delegate; RemovalObserver(ConnectObserver observer) { this.delegate = observer; } public void shutdown() { if(removeConnectObserver(delegate)) { inspectable.incrementCancelledRequestCount(); delegate.shutdown(); } } } /** A ConnectObserver to maintain the _socketsConnecting variable. */ private class DelegateConnector implements ConnectObserver { private final ConnectObserver delegate; DelegateConnector(ConnectObserver observer) { delegate = observer; } public void handleConnect(Socket s) throws IOException { releaseSocket(); delegate.handleConnect(s); } public void shutdown() { releaseSocket(); delegate.shutdown(); } // unused. public void handleIOException(IOException x) {} } /** Simple struct to hold data for non-blocking waiting requests. */ private static class Requestor { private final InetSocketAddress addr; private final int timeout; private final NBSocket socket; private final ConnectObserver observer; private final long creationTime; Requestor(NBSocket socket, InetSocketAddress addr, int timeout, ConnectObserver observer) { this.socket = socket; this.addr = addr; this.timeout = timeout; this.observer = observer; this.creationTime = System.currentTimeMillis(); } } /** Inspections related to the queue of connection attempt requestors */ private static class LimitedSocketInspectable implements Inspectable { private int maxConnectRequestsInQueue = 0; /** Maximum number of requests in the queue */ private int numberOfCancelledRequests = 0; /** Num of requests in queue cancelled before conn attempted */ private int totalQueueRequestsProcessed = 0; /** Total requests that have been gone thru the queue */ private long maxTimeSpentInQueue = 0L; /** Maximum time 1 request has spent in the queue */ private long totalWaitTimeInQueue = 0L; /** Total time all requests have spent in the queue */ /** Maximum time 1 request has spent in the queue */ public synchronized Object inspect() { Map<String,Object> ret = new HashMap<String,Object>(); ret.put("req_processed", totalQueueRequestsProcessed); ret.put("max_requests_in_queue", maxConnectRequestsInQueue); ret.put("max_time_in_queue", maxTimeSpentInQueue); ret.put("req_cancelled", numberOfCancelledRequests); ret.put("total_time_in_queue", totalWaitTimeInQueue); return ret; } synchronized void setMaxConnReqInQueueIfNecessary(int newMax) { if (newMax > maxConnectRequestsInQueue) { maxConnectRequestsInQueue = newMax; } } synchronized void incrementCancelledRequestCount() { numberOfCancelledRequests++; } synchronized void addReadyRequest(Requestor request) { totalQueueRequestsProcessed++; long timeSpent = System.currentTimeMillis() - request.creationTime; totalWaitTimeInQueue += timeSpent; if (timeSpent > this.maxTimeSpentInQueue) { maxTimeSpentInQueue = timeSpent; } } } }