package com.limegroup.gnutella.downloader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.MessageListener;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.ReplyHandler;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.UDPPinger;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.vendor.HeadPing;
import com.limegroup.gnutella.messages.vendor.HeadPong;
import com.limegroup.gnutella.settings.DownloadSettings;
import com.limegroup.gnutella.util.Cancellable;
import com.limegroup.gnutella.util.DualIterator;
import com.limegroup.gnutella.util.IpPort;
public class PingRanker extends SourceRanker implements MessageListener, Cancellable {
private static final Log LOG = LogFactory.getLog(PingRanker.class);
/**
* the pinger to send the pings
*/
private UDPPinger pinger;
/**
* new hosts (as RFDs) that we've learned about
*/
private Set newHosts;
/**
* Mapping IpPort -> RFD to which we have sent pings.
* Whenever we send pings to push proxies, each proxy points to the same
* RFD. Used to check whether we receive a pong from someone we have sent
* a ping to.
*/
private TreeMap pingedHosts;
/**
* A set containing the unique remote file locations that we have pinged. It
* differs from pingedHosts because it contains only RemoteFileDesc objects
*/
private Set testedLocations;
/**
* RFDs that have responded to our pings.
*/
private TreeSet verifiedHosts;
/**
* The urn to use to create pings
*/
private URN sha1;
/**
* The guid to use for my headPings
*/
private GUID myGUID;
/**
* whether the ranker has been stopped.
*/
private boolean running;
/**
* The last time we sent a bunch of hosts for pinging.
*/
private long lastPingTime;
private static final Comparator RFD_COMPARATOR = new RFDComparator();
private static final Comparator ALT_DEPRIORITIZER = new RFDAltDeprioritizer();
protected PingRanker() {
pinger = new UDPPinger();
pingedHosts = new TreeMap(IpPort.COMPARATOR);
testedLocations = new HashSet();
newHosts = new HashSet();
verifiedHosts = new TreeSet(RFD_COMPARATOR);
}
public synchronized boolean addToPool(Collection c) {
List l;
if (c instanceof List)
l = (List)c;
else
l = new ArrayList(c);
Collections.sort(l,ALT_DEPRIORITIZER);
return addInternal(l);
}
/**
* adds the collection of hosts to to the internal structures
*/
private boolean addInternal(Collection c) {
boolean ret = false;
for (Iterator iter = c.iterator(); iter.hasNext();) {
if (addInternal((RemoteFileDesc)iter.next()))
ret = true;
}
pingNewHosts();
return ret;
}
public synchronized boolean addToPool(RemoteFileDesc host){
boolean ret = addInternal(host);
pingNewHosts();
return ret;
}
private boolean addInternal(RemoteFileDesc host) {
// initialize the sha1 if we don't have one
if (sha1 == null) {
if( host.getSHA1Urn() != null)
sha1 = host.getSHA1Urn();
else // BUGFIX: We can't discard sources w/out a SHA1 when we dont' have
// a SHA1 for the download, or else it won't be possible to download a
// file from a query hit without a SHA1, if we can received UDP pings
return testedLocations.add(host); // we can't do anything yet
}
// do not allow duplicate hosts
if (running && knowsAboutHost(host))
return false;
if(LOG.isDebugEnabled())
LOG.debug("adding new host "+host+" "+host.getPushAddr());
boolean ret = false;
// don't bother ranking multicasts
if (host.isReplyToMulticast())
ret = verifiedHosts.add(host);
else
ret = newHosts.add(host); // rank
// make sure that if we were stopped, we return true
ret = ret | !running;
// initialize the guid if we don't have one
if (myGUID == null && meshHandler != null) {
myGUID = new GUID(GUID.makeGuid());
RouterService.getMessageRouter().registerMessageListener(myGUID.bytes(),this);
}
return ret;
}
private boolean knowsAboutHost(RemoteFileDesc host) {
return newHosts.contains(host) ||
verifiedHosts.contains(host) ||
testedLocations.contains(host);
}
public synchronized RemoteFileDesc getBest() throws NoSuchElementException {
if (!hasMore())
return null;
RemoteFileDesc ret;
// try a verified host
if (!verifiedHosts.isEmpty()){
LOG.debug("getting a verified host");
ret =(RemoteFileDesc) verifiedHosts.first();
verifiedHosts.remove(ret);
}
else {
LOG.debug("getting a non-verified host");
// use the legacy ranking logic to select a non-verified host
Iterator dual = new DualIterator(testedLocations.iterator(),newHosts.iterator());
ret = LegacyRanker.getBest(dual);
newHosts.remove(ret);
testedLocations.remove(ret);
if (ret.needsPush()) {
for (Iterator iter = ret.getPushProxies().iterator(); iter.hasNext();)
pingedHosts.remove(iter.next());
} else
pingedHosts.remove(ret);
}
pingNewHosts();
if (LOG.isDebugEnabled())
LOG.debug("the best host we came up with is "+ret+" "+ret.getPushAddr());
return ret;
}
/**
* pings a bunch of hosts if necessary
*/
private void pingNewHosts() {
// if we have reached our desired # of altlocs, don't ping
if (isCancelled())
return;
// if we don't have anybody to ping, don't ping
if (!hasNonBusy())
return;
// if we haven't found a single RFD with URN, don't ping anybody
if (sha1 == null)
return;
// if its not time to ping yet, don't ping
// use the same interval as workers for now
long now = System.currentTimeMillis();
if (now - lastPingTime < DownloadSettings.WORKER_INTERVAL.getValue())
return;
// create a ping for the non-firewalled hosts
HeadPing ping = new HeadPing(myGUID,sha1,getPingFlags());
// prepare a batch of hosts to ping
int batch = DownloadSettings.PING_BATCH.getValue();
List toSend = new ArrayList(batch);
int sent = 0;
for (Iterator iter = newHosts.iterator(); iter.hasNext() && sent < batch;) {
RemoteFileDesc rfd = (RemoteFileDesc) iter.next();
if (rfd.isBusy(now))
continue;
iter.remove();
if (rfd.needsPush()) {
if (rfd.getPushProxies().size() > 0 && rfd.getSHA1Urn() != null)
pingProxies(rfd);
} else {
pingedHosts.put(rfd,rfd);
toSend.add(rfd);
}
testedLocations.add(rfd);
sent++;
}
if (LOG.isDebugEnabled()) {
LOG.debug("\nverified hosts " +verifiedHosts.size()+
"\npingedHosts "+pingedHosts.values().size()+
"\nnewHosts "+newHosts.size()+
"\npinging hosts: "+sent);
}
pinger.rank(toSend,null,this,ping);
lastPingTime = now;
}
protected Collection getPotentiallyBusyHosts() {
return newHosts;
}
/**
* schedules a push ping to each proxy of the given host
*/
private void pingProxies(RemoteFileDesc rfd) {
if (RouterService.acceptedIncomingConnection() ||
(RouterService.getUdpService().canDoFWT() && rfd.supportsFWTransfer())) {
HeadPing pushPing =
new HeadPing(myGUID,rfd.getSHA1Urn(),
new GUID(rfd.getPushAddr().getClientGUID()),getPingFlags());
for (Iterator iter = rfd.getPushProxies().iterator(); iter.hasNext();)
pingedHosts.put(iter.next(),rfd);
if (LOG.isDebugEnabled())
LOG.debug("pinging push location "+rfd.getPushAddr());
pinger.rank(rfd.getPushProxies(),null,this,pushPing);
}
}
/**
* @return the appropriate ping flags based on current conditions
*/
private static int getPingFlags() {
int flags = HeadPing.INTERVALS | HeadPing.ALT_LOCS;
if (RouterService.acceptedIncomingConnection() ||
RouterService.getUdpService().canDoFWT())
flags |= HeadPing.PUSH_ALTLOCS;
return flags;
}
public synchronized boolean hasMore() {
return !(verifiedHosts.isEmpty() && newHosts.isEmpty() && testedLocations.isEmpty());
}
/**
* Informs the Ranker that a host has replied with a HeadPing
*/
public void processMessage(Message m, ReplyHandler handler) {
MeshHandler mesh;
RemoteFileDesc rfd;
Collection alts = null;
// this -> meshHandler NOT ok
synchronized(this) {
if (!running)
return;
if (! (m instanceof HeadPong))
return;
HeadPong pong = (HeadPong)m;
if (!pingedHosts.containsKey(handler))
return;
rfd = (RemoteFileDesc)pingedHosts.remove(handler);
testedLocations.remove(rfd);
if (LOG.isDebugEnabled()) {
LOG.debug("received a pong "+ pong+ " from "+handler +
" for rfd "+rfd+" with PE "+rfd.getPushAddr());
}
// older push proxies do not route but respond directly, we want to get responses
// from other push proxies
if (!pong.hasFile() && !pong.isGGEPPong() && rfd.needsPush())
return;
// if the pong is firewalled, remove the other proxies from the
// pinged set
if (pong.isFirewalled()) {
for (Iterator iter = rfd.getPushProxies().iterator(); iter.hasNext();)
pingedHosts.remove(iter.next());
}
mesh = meshHandler;
if (pong.hasFile()) {
//update the rfd with information from the pong
pong.updateRFD(rfd);
// if the remote host is busy, re-add him for later ranking
if (rfd.isBusy())
newHosts.add(rfd);
else
verifiedHosts.add(rfd);
alts = pong.getAllLocsRFD(rfd);
}
}
// if the pong didn't have the file, drop it
// otherwise add any altlocs the pong had to our known hosts
if (alts == null)
mesh.informMesh(rfd,false);
else
mesh.addPossibleSources(alts);
}
public synchronized void registered(byte[] guid) {
if (LOG.isDebugEnabled())
LOG.debug("ranker registered with guid "+(new GUID(guid)).toHexString(),new Exception());
running = true;
}
public synchronized void unregistered(byte[] guid) {
if (LOG.isDebugEnabled())
LOG.debug("ranker unregistered with guid "+(new GUID(guid)).toHexString(),new Exception());
running = false;
newHosts.addAll(verifiedHosts);
newHosts.addAll(testedLocations);
verifiedHosts.clear();
pingedHosts.clear();
testedLocations.clear();
lastPingTime = 0;
}
public synchronized boolean isCancelled(){
return !running || verifiedHosts.size() >= DownloadSettings.MAX_VERIFIED_HOSTS.getValue();
}
protected synchronized void clearState(){
if (myGUID != null) {
RouterService.getMessageRouter().unregisterMessageListener(myGUID.bytes(),this);
myGUID = null;
}
}
protected synchronized Collection getShareableHosts(){
List ret = new ArrayList(verifiedHosts.size()+newHosts.size()+testedLocations.size());
ret.addAll(verifiedHosts);
ret.addAll(newHosts);
ret.addAll(testedLocations);
return ret;
}
public synchronized int getNumKnownHosts() {
return verifiedHosts.size()+newHosts.size()+testedLocations.size();
}
/**
* class that actually does the preferencing of RFDs
*/
private static final class RFDComparator implements Comparator {
public int compare(Object a, Object b) {
RemoteFileDesc pongA = (RemoteFileDesc)a;
RemoteFileDesc pongB = (RemoteFileDesc)b;
// Multicasts are best
if (pongA.isReplyToMulticast() != pongB.isReplyToMulticast()) {
if (pongA.isReplyToMulticast())
return -1;
else
return 1;
}
// HeadPongs with highest number of free slots get the highest priority
if (pongA.getQueueStatus() > pongB.getQueueStatus())
return 1;
else if (pongA.getQueueStatus() < pongB.getQueueStatus())
return -1;
// Within the same queue rank, firewalled hosts get priority
if (pongA.needsPush() != pongB.needsPush()) {
if (pongA.needsPush())
return -1;
else
return 1;
}
// Within the same queue/fwall, partial hosts get priority
if (pongA.isPartialSource() != pongB.isPartialSource()) {
if (pongA.isPartialSource())
return -1;
else
return 1;
}
// the two pongs seem completely the same
return pongA.hashCode() - pongB.hashCode();
}
}
/**
* a ranker that deprioritizes RFDs from altlocs, used to make sure
* we ping the hosts that actually returned results first
*/
private static final class RFDAltDeprioritizer implements Comparator {
public int compare(Object a, Object b) {
RemoteFileDesc rfd1 = (RemoteFileDesc)a;
RemoteFileDesc rfd2 = (RemoteFileDesc)b;
if (rfd1.isFromAlternateLocation() != rfd2.isFromAlternateLocation()) {
if (rfd1.isFromAlternateLocation())
return 1;
else
return -1;
}
return 0;
}
}
}