package com.limegroup.gnutella;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import com.limegroup.gnutella.guess.GUESSEndpoint;
import com.limegroup.gnutella.guess.QueryKey;
import com.limegroup.gnutella.messages.PingReply;
import com.limegroup.gnutella.messages.PingRequest;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.settings.SearchSettings;
import com.limegroup.gnutella.statistics.SentMessageStatHandler;
import com.limegroup.gnutella.util.Buffer;
import com.limegroup.gnutella.util.ManagedThread;
import com.limegroup.gnutella.util.NetworkUtils;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
/**
* This class runs a single thread which sends unicast UDP queries to a master
* list of unicast-enabled hosts every n milliseconds. It interacts with
* HostCatcher to find unicast-enabled hosts. It also allows for stopping of
* individual queries by reply counts.
*/
public final class QueryUnicaster {
private static final Log LOG = LogFactory.getLog(QueryUnicaster.class);
/** The time in between successive unicast queries.
*/
public static final int ITERATION_TIME = 100; // 1/10th of a second...
/** The number of Endpoints where you should start sending pings to them.
*/
public static final int MIN_ENDPOINTS = 25;
/** The max number of unicast pongs to store.
*/
//public static final int MAX_ENDPOINTS = 2000;
public static final int MAX_ENDPOINTS = 30;
/** One hour in milliseconds.
*/
public static final long ONE_HOUR = 1000 * 60 * 60; // 60 minutes
// the instance of me....
private final static QueryUnicaster _instance = new QueryUnicaster();
/** Actually sends any QRs via unicast UDP messages.
*/
private Thread _querier = null;
// should the _querier be running?
private boolean _shouldRun = true;
/**
* The map of Queries I need to send every iteration.
* The map is from GUID to QueryBundle. The following invariant is
* maintained:
* GUID -> QueryBundle where GUID == QueryBundle._qr.getGUID()
*/
private Map _queries;
/**
* Maps leaf connections to the queries they've spawned.
* The map is from ReplyHandler to a Set (of GUIDs).
*/
private Map _querySets;
/**
* The unicast enabled hosts I should contact for queries. Add to the
* front, remove from the end. Therefore, the OLDEST entries are at the
* end.
*/
private LinkedList _queryHosts;
/**
* The Set of QueryKeys to be used for Queries.
* GUESSEndpoint -> QueryKey
*/
private Map _queryKeys;
/** The fixed size list of endpoints i've pinged.
*/
private Buffer _pingList;
/** A List of query GUIDS to purge.
*/
private List _qGuidsToRemove;
/** The last time I sent a broadcast ping.
*/
private long _lastPingTime = 0;
/**
* Variable for how many test pings have been sent out to determine
* whether or not we can accept incoming connections.
*/
private int _testUDPPingsSent = 0;
/**
* Records whether or not someone has called init on me....
*/
private boolean _initialized = false;
/** Need to call initialize() to make sure I'm running!
*/
public static QueryUnicaster instance() {
return _instance;
}
//----------------------------------------------------
// These methods are used by the QueryUnicasterTester.
// That is why they are package level. In general
// they should not be used by others, though it is
// technically OK
/** Returns the number of Queries unicasted by this guy...
*/
int getQueryNumber() {
return _queries.size();
}
//----------------------------------------------------
/**
* Returns a List of unicast Endpoints. These Endpoints are the NEWEST
* we've seen.
*/
public List getUnicastEndpoints() {
List retList = new ArrayList();
synchronized (_queryHosts) {
LOG.debug("QueryUnicaster.getUnicastEndpoints(): obtained lock.");
int size = _queryHosts.size();
if (size > 0) {
int max = (size > 10 ? 10 : size);
for (int i = 0; i < max; i++)
retList.add(_queryHosts.get(i));
}
LOG.debug("QueryUnicaster.getUnicastEndpoints(): releasing lock.");
}
return retList;
}
/**
* Returns a <tt>GUESSEndpoint</tt> from the current cache of
* GUESS endpoints.
*
* @return a <tt>GUESSEndpoint</tt> from the list of GUESS hosts
* to query, or <tt>null</tt> if there are no available hosts
* to return
*/
public GUESSEndpoint getUnicastEndpoint() {
synchronized(_queryHosts) {
if(_queryHosts.isEmpty()) return null;
return (GUESSEndpoint)_queryHosts.getFirst();
}
}
/**
* Constructs a new <tt>QueryUnicaster</tt> and starts its query loop.
*/
private QueryUnicaster() {
// construct DSes...
_queries = new Hashtable();
_queryHosts = new LinkedList();
_queryKeys = new Hashtable();
_pingList = new Buffer(25);
_querySets = new Hashtable();
_qGuidsToRemove = new Vector();
// start service...
_querier = new ManagedThread() {
public void managedRun() {
queryLoop();
}
};
_querier.setName("QueryUnicaster");
_querier.setDaemon(true);
}
/**
* Starts the query unicaster thread.
*/
public synchronized void start() {
if (!_initialized) {
_querier.start();
QueryKeyExpirer expirer = new QueryKeyExpirer();
RouterService.schedule(expirer, 0, 3 * ONE_HOUR);// every 3 hours
_initialized = true;
}
}
/**
* The main work to be done.
* If there are queries, get a unicast enabled UP, and send each Query to
* it. Then sleep and try some more later...
*/
private void queryLoop() {
UDPService udpService = UDPService.instance();
while (_shouldRun) {
try {
waitForQueries();
GUESSEndpoint toQuery = getUnicastHost();
// no query key to use in my query!
if (!_queryKeys.containsKey(toQuery)) {
// send a QueryKey Request
PingRequest pr = PingRequest.createQueryKeyRequest();
udpService.send(pr,toQuery.getAddress(), toQuery.getPort());
SentMessageStatHandler.UDP_PING_REQUESTS.addMessage(pr);
// DO NOT RE-ADD ENDPOINT - we'll do that if we get a
// QueryKey Reply!!
continue; // try another up above....
}
QueryKey queryKey =
((QueryKeyBundle) _queryKeys.get(toQuery))._queryKey;
purgeGuidsInternal(); // in case any were added while asleep
boolean currentHostUsed = false;
synchronized (_queries) {
Iterator iter = _queries.values().iterator();
while (iter.hasNext()) {
QueryBundle currQB = (QueryBundle)iter.next();
if (currQB._hostsQueried.size() > QueryBundle.MAX_QUERIES)
// query is now stale....
_qGuidsToRemove.add(new GUID(currQB._qr.getGUID()));
else if (currQB._hostsQueried.contains(toQuery))
; // don't send another....
else {
InetAddress ip = toQuery.getAddress();
QueryRequest qrToSend =
QueryRequest.createQueryKeyQuery(currQB._qr,
queryKey);
udpService.send(qrToSend,
ip, toQuery.getPort());
currentHostUsed = true;
SentMessageStatHandler.UDP_QUERY_REQUESTS.addMessage(qrToSend);
currQB._hostsQueried.add(toQuery);
}
}
}
// add the current host back to the list if it was not used for
// any query
if(!currentHostUsed) {
addUnicastEndpoint(toQuery);
}
// purge stale queries, hold lock so you don't miss any...
synchronized (_qGuidsToRemove) {
purgeGuidsInternal();
_qGuidsToRemove.clear();
}
Thread.sleep(ITERATION_TIME);
}
catch (InterruptedException ignored) {}
}
}
/**
* A quick purging of query GUIDS from the _queries Map. The
* queryLoop uses this to so it doesn't have to hold the _queries
* lock for too long.
*/
private void purgeGuidsInternal() {
synchronized (_qGuidsToRemove) {
Iterator removee = _qGuidsToRemove.iterator();
while (removee.hasNext()) {
GUID currGuid = (GUID) removee.next();
_queries.remove(currGuid);
}
}
}
private void waitForQueries() throws InterruptedException {
LOG.debug("QueryUnicaster.waitForQueries(): waiting for Queries.");
synchronized (_queries) {
if (_queries.isEmpty()) {
// i'll be notifed when stuff is added...
_queries.wait();
}
}
if(LOG.isDebugEnabled())
LOG.debug("QueryUnicaster.waitForQueries(): numQueries = " +
_queries.size());
}
/**
* @return true if the query was added (maybe false if it existed).
* @param query The Query to add, to start unicasting.
* @param reference The originating connection. OK if NULL.
*/
public boolean addQuery(QueryRequest query, ReplyHandler reference) {
LOG.debug("QueryUnicaster.addQuery(): entered.");
boolean retBool = false;
GUID guid = new GUID(query.getGUID());
// first map the QueryBundle using the guid....
synchronized (_queries) {
if (!_queries.containsKey(guid)) {
QueryBundle qb = new QueryBundle(query);
_queries.put(guid, qb);
retBool = true;
}
if (retBool) {
_queries.notifyAll();
}
}
// return if this node originated the query
if (reference == null)
return retBool;
// then record the guid in the set of leaf's queries...
synchronized (_querySets) {
Set guids = (Set) _querySets.get(reference);
if (guids == null) {
guids = new HashSet();
_querySets.put(reference, guids);
}
guids.add(guid);
}
if(LOG.isDebugEnabled())
LOG.debug("QueryUnicaster.addQuery(): returning " + retBool);
return retBool;
}
/** Just feed me ExtendedEndpoints - I'll check if I could use them or not.
*/
public void addUnicastEndpoint(InetAddress address, int port) {
if (!SearchSettings.GUESS_ENABLED.getValue()) return;
if (notMe(address, port) && NetworkUtils.isValidPort(port) &&
NetworkUtils.isValidAddress(address)) {
GUESSEndpoint endpoint = new GUESSEndpoint(address, port);
addUnicastEndpoint(endpoint);
}
}
/** Adds the <tt>GUESSEndpoint</tt> instance to the host data.
*
* @param endpoint the <tt>GUESSEndpoint</tt> to add
*/
private void addUnicastEndpoint(GUESSEndpoint endpoint) {
synchronized (_queryHosts) {
LOG.debug("QueryUnicaster.addUnicastEndpoint(): obtained lock.");
if (_queryHosts.size() == MAX_ENDPOINTS)
_queryHosts.removeLast(); // evict a old guy...
_queryHosts.addFirst(endpoint);
_queryHosts.notify();
if(UDPService.instance().isListening() &&
!RouterService.isGUESSCapable() &&
(_testUDPPingsSent < 10) &&
!(ConnectionSettings.LOCAL_IS_PRIVATE.getValue() &&
NetworkUtils.isCloseIP(RouterService.getAddress(),
endpoint.getAddress().getAddress())) ) {
PingRequest pr =
new PingRequest(UDPService.instance().getSolicitedGUID().bytes(),
(byte)1, (byte)0);
UDPService.instance().send(pr, endpoint.getAddress(),
endpoint.getPort());
SentMessageStatHandler.UDP_PING_REQUESTS.addMessage(pr);
_testUDPPingsSent++;
}
LOG.debug("QueryUnicaster.addUnicastEndpoint(): released lock.");
}
}
/**
* Returns whether or not the Endpoint refers to me! True if it doesn't,
* false if it does (NOT not me == me).
*/
private boolean notMe(InetAddress address, int port) {
boolean retVal = true;
if ((port == RouterService.getPort()) &&
Arrays.equals(address.getAddress(),
RouterService.getAddress())) {
retVal = false;
}
return retVal;
}
/**
* Gets rid of a Query according to ReplyHandler.
* Use this if a leaf connection dies and you want to stop the query.
*/
void purgeQuery(ReplyHandler reference) {
LOG.debug("QueryUnicaster.purgeQuery(RH): entered.");
if (reference == null)
return;
synchronized (_querySets) {
Set guids = (Set) _querySets.remove(reference);
if (guids == null)
return;
Iterator iter = guids.iterator();
while (iter.hasNext())
purgeQuery((GUID) iter.next());
}
LOG.debug("QueryUnicaster.purgeQuery(RH): returning.");
}
/**
* Gets rid of a Query according to GUID. Use this if a leaf connection
* dies and you want to stop the query.
*/
void purgeQuery(GUID queryGUID) {
LOG.debug("QueryUnicaster.purgeQuery(GUID): entered.");
_qGuidsToRemove.add(queryGUID);
LOG.debug("QueryUnicaster.purgeQuery(GUID): returning.");
}
/** Feed me QRs so I can keep track of stuff.
*/
public void handleQueryReply(QueryReply qr) {
addResults(new GUID(qr.getGUID()), qr.getResultCount());
}
/** Feed me QueryKey pongs so I can query people....
* pre: pr.getQueryKey() != null
*/
public void handleQueryKeyPong(PingReply pr) {
if(pr == null) {
throw new NullPointerException("null pong");
}
QueryKey qk = pr.getQueryKey();
if(qk == null) {
throw new IllegalArgumentException("no key in pong");
}
InetAddress address = pr.getInetAddress();
Assert.that(qk != null);
int port = pr.getPort();
GUESSEndpoint endpoint = new GUESSEndpoint(address, port);
_queryKeys.put(endpoint, new QueryKeyBundle(qk));
addUnicastEndpoint(endpoint);
}
/**
* Add results to a query so we can invalidate it when enough results are
* received.
*/
private void addResults(GUID queryGUID, int numResultsToAdd) {
synchronized (_queries) {
QueryBundle qb = (QueryBundle) _queries.get(queryGUID);
if (qb != null) {// add results if possible...
qb._numResults += numResultsToAdd;
// This code moved from queryLoop() since that ftn. blocks before
// removing stale queries, when out of hosts to query.
if( qb._numResults>QueryBundle.MAX_RESULTS ) {
synchronized( _qGuidsToRemove ) {
_qGuidsToRemove.add(new GUID(qb._qr.getGUID()));
purgeGuidsInternal();
_qGuidsToRemove.clear();
}
}
}
}
}
/** May block if no hosts exist.
*/
private GUESSEndpoint getUnicastHost() throws InterruptedException {
LOG.debug("QueryUnicaster.getUnicastHost(): waiting for hosts.");
synchronized (_queryHosts) {
LOG.debug("QueryUnicaster.getUnicastHost(): obtained lock.");
while (_queryHosts.isEmpty()) {
if ((System.currentTimeMillis() - _lastPingTime) >
20000) { // don't sent too many pings..
// first send a Ping, hopefully we'll get some pongs....
PingRequest pr =
new PingRequest(ConnectionSettings.TTL.getValue());
RouterService.getMessageRouter().broadcastPingRequest(pr);
_lastPingTime = System.currentTimeMillis();
}
// now wait, what else can we do?
_queryHosts.wait();
}
LOG.debug("QueryUnicaster.getUnicastHost(): got a host, let go lock!");
}
if (_queryHosts.size() < MIN_ENDPOINTS) {
// send a ping to the guy you are popping if cache too small
GUESSEndpoint toReturn =
(GUESSEndpoint) _queryHosts.removeLast();
// if i haven't pinged him 'recently', then ping him...
synchronized (_pingList) {
if (!_pingList.contains(toReturn)) {
PingRequest pr = new PingRequest((byte)1);
InetAddress ip = toReturn.getAddress();
UDPService.instance().send(pr, ip, toReturn.getPort());
_pingList.add(toReturn);
SentMessageStatHandler.UDP_PING_REQUESTS.addMessage(pr);
}
}
return toReturn;
}
return (GUESSEndpoint) _queryHosts.removeLast();
}
/** removes all Unicast Endpoints, reset associated members
*/
private void resetUnicastEndpointsAndQueries() {
LOG.debug("Resetting unicast endpoints.");
synchronized (_queries) {
_queries.clear();
_queries.notifyAll();
}
synchronized (_queryHosts) {
_queryHosts.clear();
_queryHosts.notifyAll();
}
synchronized (_queryKeys) {
_queryKeys.clear();
_queryKeys.notifyAll();
}
synchronized (_pingList) {
_pingList.clear();
_pingList.notifyAll();
}
_lastPingTime=0;
_testUDPPingsSent=0;
}
private static class QueryBundle {
public static final int MAX_RESULTS = 250;
public static final int MAX_QUERIES = 1000;
final QueryRequest _qr;
// the number of results received per Query...
int _numResults = 0;
/** The Set of Endpoints queried for this Query.
*/
final Set _hostsQueried = new HashSet();
public QueryBundle(QueryRequest qr) {
_qr = qr;
}
// overrides toString to provide more information
public String toString() {
return "QueryBundle: "+_qr;
}
}
private static class QueryKeyBundle {
public static final long QUERY_KEY_LIFETIME = 2 * ONE_HOUR; // 2 hours
final long _birthTime;
final QueryKey _queryKey;
public QueryKeyBundle(QueryKey qk) {
_queryKey = qk;
_birthTime = System.currentTimeMillis();
}
/** Returns true if this QueryKey hasn't been updated in a while and
* should be expired.
*/
public boolean shouldExpire() {
if ((System.currentTimeMillis() - _birthTime) >=
QUERY_KEY_LIFETIME)
return true;
return false;
}
public String toString() {
return "{QueryKeyBundle: " + _queryKey + " BirthTime = " +
_birthTime;
}
}
/**
* Schedule this class to run every so often and rid the Map of Bundles that
* are stale.
*/
private class QueryKeyExpirer implements Runnable {
public void run() {
synchronized (_queryKeys) {
Set entries = _queryKeys.entrySet();
Iterator iter = entries.iterator();
while (iter.hasNext()) {
QueryKeyBundle currQKB = (QueryKeyBundle) iter.next();
if (currQKB.shouldExpire())
entries.remove(currQKB);
}
}
}
}
}