package com.limegroup.gnutella;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Collections;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.messages.PingRequest;
import com.limegroup.gnutella.settings.ConnectionSettings;
/*
* A "watchdog" that periodically examines connections and
* replaces dud connections with better ones. There are a number of
* possible heuristics to use when examining connections.
*/
public final class ConnectionWatchdog {
private static final Log LOG = LogFactory.getLog(ConnectionWatchdog.class);
/**
* Constant handle to single <tt>ConnectionWatchdog</tt> instance,
* following the singleton pattern.
*/
private static final ConnectionWatchdog INSTANCE = new ConnectionWatchdog();
/** How long (in msec) a connection can be a dud (see below) before being booted. */
private static final int EVALUATE_TIME=30000;
/** Additional time (in msec) to wait before rechecking connections. */
private static final int REEVALUATE_TIME=15000;
/**
* Singleton accessor for <tt>ConnectionWatchdog</tt> instance.
*/
public static ConnectionWatchdog instance() {
return INSTANCE;
}
/**
* Creates a new <tt>ConnectionWatchdog</tt> instance to monitor
* connections to make sure they are still up and responding well.
*
* @param manager the <tt>ConnectionManager</tt> instance that provides
* access to the list of connections to monitor
*/
private ConnectionWatchdog() {}
/**
* Starts the <tt>ConnectionWatchdog</tt>.
*/
public void start() {
findDuds();
}
/** A snapshot of a connection. */
private static class ConnectionState {
final long sentDropped;
final long sent;
final long received;
/** Takes a snapshot of the given connection. */
ConnectionState(ManagedConnection c) {
this.sentDropped=c.getNumSentMessagesDropped();
this.sent=c.getNumMessagesSent();
this.received=c.getNumMessagesReceived();
}
/**
* Returns true if the state of this connection has not
* made sufficient progress since the old snapshot was taken.
*/
boolean notProgressedSince(ConnectionState old) {
//Current policy: returns true if (a) all packets sent since
//snapshot were dropped or (b) we have received no data.
long numSent=this.sent-old.sent;
long numSentDropped=this.sentDropped-old.sentDropped;
long numReceived=this.received-old.received;
if ((numSent==numSentDropped) && numSent!=0) {
return true;
} else if (numReceived==0) {
return true;
} else
return false;
}
public String toString() {
return "{sent: "+sent+", sdropped: "+sentDropped+"}";
}
}
/**
* Schedules a snapshot of connection progress to be evaluated for duds.
*/
private void findDuds() {
//Take a snapshot of all connections, including leaves.
Map /* ManagedConnection -> ConnectionState */ snapshot = new HashMap();
for (Iterator iter = allConnections(); iter.hasNext(); ) {
ManagedConnection c=(ManagedConnection)iter.next();
if (!c.isKillable())
continue;
snapshot.put(c, new ConnectionState(c));
}
RouterService.schedule(new DudChecker(snapshot, false), EVALUATE_TIME, 0);
}
/**
* Looks at a list of connections & pings them, waiting a certain amount of
* time for a response. If no messages are exchanged on the connection in
* that time, the connection is killed.
*
* This is done by scheduling an event and checking the progress against
* a snapshot.
* @requires connections is a list of ManagedConnection
* @modifies manager, router
* @effects removes from manager any ManagedConnection's in "connections"
* that still aren't progressing after being pinged.
*/
private void killIfStillDud(List connections) {
//Take a snapshot of each connection, then send a ping.
//The horizon statistics for the connection are temporarily disabled
//during this process. In the rare chance that legitimate pongs
//(other than in response to my ping), they will be ignored.
HashMap /* Connection -> ConnectionState */ snapshot = new HashMap();
for (Iterator iter = connections.iterator(); iter.hasNext(); ) {
ManagedConnection c=(ManagedConnection)iter.next();
if (!c.isKillable())
continue;
snapshot.put(c, new ConnectionState(c));
RouterService.getMessageRouter().sendPingRequest(new PingRequest((byte)1), c);
}
RouterService.schedule(new DudChecker(snapshot, true), REEVALUATE_TIME, 0);
}
/** Returns an iterator of all initialized connections in this, including
* leaf connecions. */
private Iterator allConnections() {
List normal = RouterService.getConnectionManager().getInitializedConnections();
List leaves = RouterService.getConnectionManager().getInitializedClientConnections();
List buf = new ArrayList(normal.size() + leaves.size());
buf.addAll(normal);
buf.addAll(leaves);
return buf.iterator();
}
/**
* Determines if snapshots of connections are duds.
* If 'kill' is true, if they're a dud they're immediately clue.
* Otherwise, duds are queued up for additional checking.
* If no duds exist (or they were killed), findDuds() is started again.
*/
private class DudChecker implements Runnable {
private Map snapshots;
private boolean kill;
/**
* Constructs a new DudChecker with the snapshots of ConnectionStates.
* The checker may be used to kill the connections (if they haven't progressed)
* or to re-evaluate them later.
*/
DudChecker(Map snapshots, boolean kill) {
this.snapshots = snapshots;
this.kill = kill;
}
public void run() {
//Loop through all connections, trying to find ones that
//have not made sufficient progress.
List potentials = kill ? Collections.EMPTY_LIST : new ArrayList();
for (Iterator iter=allConnections(); iter.hasNext(); ) {
ManagedConnection c = (ManagedConnection)iter.next();
if (!c.isKillable())
continue;
Object state = snapshots.get(c);
if (state == null)
continue; //this is a new connection
ConnectionState currentState=new ConnectionState(c);
ConnectionState oldState=(ConnectionState)state;
if (currentState.notProgressedSince(oldState)) {
if(kill) {
if(ConnectionSettings.WATCHDOG_ACTIVE.getValue()) {
if(LOG.isWarnEnabled())
LOG.warn("Killing connection: " + c);
RouterService.removeConnection(c);
}
} else {
if(LOG.isWarnEnabled())
LOG.warn("Potential dud: " + c);
potentials.add(c);
}
}
}
if(potentials.isEmpty())
findDuds();
else
killIfStillDud(potentials);
}
}
}