/**
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.file.connection;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mucommander.commons.file.Credentials;
import com.mucommander.commons.file.FileURL;
/**
* @see com.mucommander.commons.file.connection.ConnectionHandler
* @author Maxence Bernard
*/
public class ConnectionPool implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionPool.class);
/** Singleton instance */
private static ConnectionPool instance = new ConnectionPool();
/** List of registered ConnectionHandler */
private final static List<ConnectionHandler> connectionHandlers = new ArrayList<ConnectionHandler>();
/** The thread that monitors connections, null if there currently is no registered ConnectionHandler */
private static Thread monitorThread;
/** Controls how of often the thread monitor checks connections */
private final static int MONITOR_SLEEP_PERIOD = 1000;
/** Maximum number of simultaneous connections per realm/credentials combo */
private final static int MAX_CONNECTIONS_PER_REALM = 4;
public static ConnectionHandler getConnectionHandler(ConnectionHandlerFactory connectionHandlerFactory, FileURL url, boolean acquireLock) throws InterruptedIOException {
FileURL realm = url.getRealm();
while(true) {
synchronized(connectionHandlers) { // Ensures that monitor thread is not currently changing the list while we access it
Credentials urlCredentials = url.getCredentials();
int matchingConnHandlers = 0;
// Try and find an appropriate existing ConnectionHandler
for(ConnectionHandler connHandler : connectionHandlers) {
// ConnectionHandler must match the realm and credentials and must not be locked
if(connHandler.equals(realm, urlCredentials)) {
matchingConnHandlers++;
synchronized(connHandler) { // Ensures that lock remains unchanged while we access/update it
if(!connHandler.isLocked()) {
// Try to acquire lock if a lock was requested
if(!acquireLock || connHandler.acquireLock()) {
LOGGER.info("returning ConnectionHandler {}, realm = {}", connHandler, realm);
// Update last activity timestamp to now
connHandler.updateLastActivityTimestamp();
return connHandler;
}
}
}
}
if(matchingConnHandlers==MAX_CONNECTIONS_PER_REALM) {
LOGGER.info("Maximum number of connection per realm reached, waiting for one to be removed or released...");
try {
// Wait for a ConnectionHandler to be released or removed from the pool
connectionHandlers.wait(); // relinquishes the lock on connectionHandlers
break;
}
catch(InterruptedException e) {
LOGGER.info("Interrupted while waiting on a connection for {}", url, e);
throw new InterruptedIOException();
}
}
}
if(matchingConnHandlers==MAX_CONNECTIONS_PER_REALM)
continue;
// No suitable ConnectionHandler found, create a new one
ConnectionHandler connHandler = connectionHandlerFactory.createConnectionHandler(url);
// Acquire lock if a lock was requested
if(acquireLock)
connHandler.acquireLock();
LOGGER.info("adding new ConnectionHandler {}, realm = {}", connHandler, connHandler.getRealm());
// Insert new ConnectionHandler at first position as if it has more chances to be accessed again soon
connectionHandlers.add(0, connHandler);
// Start monitor thread if it is not currently running (if there previously was no registered ConnectionHandler)
if(monitorThread==null) {
LOGGER.info("starting monitor thread");
monitorThread = new Thread(instance);
monitorThread.start();
}
// Update last activity timestamp to now
connHandler.updateLastActivityTimestamp();
return connHandler;
}
}
}
/**
* Returns a list of registered ConnectionHandler instances. As the name of this method implies, the returned
* list is only a snapshot and will not reflect the modifications that are made after this method has been called.
* The Vector is a cloned one and thus can be safely modified.
*
* @return a list of registered ConnectionHandler instances
*/
public static List<ConnectionHandler> getConnectionHandlersSnapshot() {
synchronized (connectionHandlers) {
return new ArrayList<ConnectionHandler>(connectionHandlers);
}
}
/**
* Called by {@link ConnectionHandler#releaseLock()} to notify the <code>ConnectionHandler</code> that a
* <code>ConnectionHandler</code> has been released.
*/
static void notifyConnectionHandlerLockReleased() {
synchronized(connectionHandlers) {
// Notify any thread waiting for a ConnectionHandler to be released
connectionHandlers.notify();
}
}
/**
* Monitors connections and periodically:
* <ul>
* <li>keeps connections alive
* <li>closes and removes connections that have expired
* </ul>
*/
public void run() {
while(monitorThread!=null) { // Thread will be interrupted by CloseConnectionThread if there are no more ConnectionHandler
long now = System.currentTimeMillis();
synchronized(connectionHandlers) { // Ensures that getConnectionHandler is not currently changing the list while we access it
for (Iterator<ConnectionHandler> it = connectionHandlers.iterator(); it.hasNext(); ) {
ConnectionHandler connHandler = it.next();
synchronized(connHandler) { // Ensures that no one is trying to acquire a lock on the connection while we access it
if(!connHandler.isLocked()) { // Do not touch ConnectionHandler if it is currently locked
// Remove ConnectionHandler instance from the list of registered ConnectionHandler
// if it is not connected
if(!connHandler.isConnected()) {
LOGGER.info("Removing unconnected ConnectionHandler {}", connHandler);
it.remove();
// Notify any thread waiting for a ConnectionHandler to be released
connectionHandlers.notify();
continue; // Skips close on inactivity and keep alive checks
}
long lastUsed = connHandler.getLastActivityTimestamp();
// If time-to-live has been reached without any connection activity, remove ConnectionHandler
// from the list of registered ConnectionHandler and close the connection in a separate thread
long closePeriod = connHandler.getCloseOnInactivityPeriod();
if(closePeriod!=-1 && now-lastUsed>closePeriod*1000) {
LOGGER.info("Removing timed-out ConnectionHandler {}",connHandler);
it.remove();
// Notify any thread waiting for a ConnectionHandler to be released
connectionHandlers.notify();
// Close connection in a separate thread as it could lock this thread
new CloseConnectionThread(connHandler).start();
continue; // Skips keep alive check
}
// If keep-alive period has been reached without any connection activity or a keep alive,
// keep connection alive in a separate thread
long keepAlivePeriod = connHandler.getKeepAlivePeriod();
if(keepAlivePeriod!=-1 && now-Math.max(lastUsed, connHandler.getLastKeepAliveTimestamp())>keepAlivePeriod*1000) {
// Update last keep alive timestamp to now
connHandler.updateLastKeepAliveTimestamp();
// Keep connection alive in a separate thread as it could lock this thread
new KeepAliveConnectionThread(connHandler).start();
}
}
}
}
// Stop monitor thread if there are no more ConnectionHandler
if(connectionHandlers.isEmpty()) {
LOGGER.info("No more ConnectionHandler, stopping monitor thread");
monitorThread = null;
}
}
// Sleep for MONITOR_SLEEP_PERIOD milliseconds, minus the processing time of this loop
try {
Thread.sleep(Math.max(0, MONITOR_SLEEP_PERIOD-(System.currentTimeMillis()-now)));
}
catch(InterruptedException e) {
// Will loop again
}
}
}
/**
* Closes a specified ConnectionHandler's connection in a separate thread and removes the ConnectionHandler from
* the list of registered ConnectionHandler instances.
*/
private static class CloseConnectionThread extends Thread {
private ConnectionHandler connHandler;
private CloseConnectionThread(ConnectionHandler connHandler) {
this.connHandler = connHandler;
}
@Override
public void run() {
// Try to close connection, only if it is connected
if(connHandler.isConnected()) {
LOGGER.info("Closing connection held by {}", connHandler);
connHandler.closeConnection();
}
}
}
/**
* Keeps alive a specified ConnectionHandler's connection in a separate thread. If the connection is not currently
* active, {@link com.mucommander.commons.file.connection.ConnectionHandler#keepAlive()} will not be called.
*/
private static class KeepAliveConnectionThread extends Thread {
private final ConnectionHandler connHandler;
private KeepAliveConnectionThread(ConnectionHandler connHandler) {
this.connHandler = connHandler;
}
@Override
public void run() {
LOGGER.info("keeping connection alive: {}", connHandler);
synchronized(connHandler) {
// Ensures that lock was not grabbed in the meantime
if(connHandler.isLocked())
return;
// Keep alive connection, only if it is connected
if(connHandler.isConnected())
connHandler.keepAlive();
}
}
}
}