package com.limegroup.gnutella.bootstrap;
import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.ExtendedEndpoint;
import com.limegroup.gnutella.UDPPinger;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.MessageListener;
import com.limegroup.gnutella.ReplyHandler;
import com.limegroup.gnutella.UDPReplyHandler;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PingRequest;
import com.limegroup.gnutella.util.IpPortSet;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.util.Cancellable;
import com.limegroup.gnutella.util.FixedSizeExpiringSet;
import java.io.Writer;
import java.io.IOException;
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
/**
* A collection of UDP Host Caches.
*/
public class UDPHostCache {
private static final Log LOG = LogFactory.getLog(UDPHostCache.class);
/**
* The maximum number of failures to allow for a given cache.
*/
private static final int MAXIMUM_FAILURES = 5;
/**
* The total number of udp host caches to remember between
* launches, or at any given time.
*/
public static final int PERMANENT_SIZE = 100;
/**
* The number of hosts we try to fetch from at once.
*/
public static final int FETCH_AMOUNT = 5;
/**
* A list of UDP Host caches, to allow easy sorting & randomizing.
* For convenience, a Set is also maintained, to easily look up duplicates.
* INVARIANT: udpHosts contains no duplicates and contains exactly
* the same elements and udpHostsSet
* LOCKING: obtain this' monitor before modifying either */
private final List /* of ExtendedEndpoint */ udpHosts =
new ArrayList(PERMANENT_SIZE);
private final Set /* of ExtendedEndpoint */ udpHostsSet = new HashSet();
private final UDPPinger pinger;
/**
* A set of hosts who we've recently contacted, so we don't contact them
* again.
*/
private final Set /* of ExtendedEndpoint */ attemptedHosts;
/**
* Whether or not we need to resort the udpHosts by failures.
*/
private boolean dirty = false;
/**
* Whether or not the set contains data different than when we last wrote.
*/
private boolean writeDirty = false;
/**
* Constructs a new UDPHostCache that remembers attempting hosts for 10
* minutes.
*/
public UDPHostCache(UDPPinger pinger) {
this(10 * 60 * 1000,pinger);
}
/**
* Constructs a new UDPHostCache that remembers attempting hosts for
* the given amount of time, in msecs.
*/
public UDPHostCache(long expiryTime,UDPPinger pinger) {
attemptedHosts = new FixedSizeExpiringSet(PERMANENT_SIZE, expiryTime);
this.pinger = pinger;
}
/**
* Writes this' info out to the stream.
*/
public synchronized void write(Writer out) throws IOException {
for(Iterator iter = udpHosts.iterator(); iter.hasNext(); ) {
ExtendedEndpoint e = (ExtendedEndpoint)iter.next();
e.write(out);
}
writeDirty = false;
}
/**
* Determines if data has been dirtied since the last time we wrote.
*/
public synchronized boolean isWriteDirty() {
return writeDirty;
}
/**
* Returns the number of UDP Host Caches this knows about.
*/
public synchronized int getSize() {
return udpHostsSet.size();
}
/**
* Erases the attempted hosts & decrements the failure counts.
*/
public synchronized void resetData() {
LOG.debug("Clearing attempted udp host caches");
decrementFailures();
attemptedHosts.clear();
}
/**
* Decrements the failure count for each known cache.
*/
protected synchronized void decrementFailures() {
for(Iterator i = attemptedHosts.iterator(); i.hasNext(); ) {
ExtendedEndpoint ep = (ExtendedEndpoint)i.next();
ep.decrementUDPHostCacheFailure();
// if we brought this guy down back to a managable
// failure size, add'm back if we have room.
if(ep.getUDPHostCacheFailures() == MAXIMUM_FAILURES &&
udpHosts.size() < PERMANENT_SIZE)
add(ep);
dirty = true;
writeDirty = true;
}
}
/**
* Attempts to contact a host cache to retrieve endpoints.
*
* Contacts 10 UDP hosts at a time.
*/
public synchronized boolean fetchHosts() {
// If the order has possibly changed, resort.
if(dirty) {
// shuffle then sort, ensuring that we're still going to use
// hosts in order of failure, but within each of those buckets
// the order will be random.
Collections.shuffle(udpHosts);
Collections.sort(udpHosts, FAILURE_COMPARATOR);
dirty = false;
}
// Keep only the first FETCH_AMOUNT of the valid hosts.
List validHosts = new ArrayList(Math.min(FETCH_AMOUNT, udpHosts.size()));
List invalidHosts = new LinkedList();
for(Iterator i = udpHosts.iterator(); i.hasNext() && validHosts.size() < FETCH_AMOUNT; ) {
Object next = i.next();
if(attemptedHosts.contains(next))
continue;
// if it was private (couldn't look up too) drop it.
if(NetworkUtils.isPrivateAddress(((ExtendedEndpoint)next).getAddress())) {
invalidHosts.add(next);
continue;
}
validHosts.add(next);
}
// Remove all invalid hosts.
for(Iterator i = invalidHosts.iterator(); i.hasNext(); )
remove((ExtendedEndpoint)i.next());
attemptedHosts.addAll(validHosts);
return fetch(validHosts);
}
/**
* Fetches endpoints from the given collection of hosts.
*/
protected synchronized boolean fetch(Collection hosts) {
if(hosts.isEmpty()) {
LOG.debug("No hosts to fetch");
return false;
}
if(LOG.isDebugEnabled())
LOG.debug("Fetching endpoints from " + hosts + " host caches");
pinger.rank(
hosts,
new HostExpirer(hosts),
// cancel when connected -- don't send out any more pings
new Cancellable() {
public boolean isCancelled() {
return RouterService.isConnected();
}
},
getPing()
);
return true;
}
/**
* Returns a PingRequest to be used while fetching.
*
* Useful as a seperate method for tests to catch the Ping's GUID.
*/
protected PingRequest getPing() {
return PingRequest.createUHCPing();
}
/**
* Removes a given hostcache from this.
*/
public synchronized boolean remove(ExtendedEndpoint e) {
if(LOG.isTraceEnabled())
LOG.trace("Removing endpoint: " + e);
boolean removed1=udpHosts.remove(e);
boolean removed2=udpHostsSet.remove(e);
Assert.that(removed1==removed2,
"Set "+removed1+" but queue "+removed2);
if(removed1)
writeDirty = true;
return removed1;
}
/**
* Adds a new udp hostcache to this.
*/
public synchronized boolean add(ExtendedEndpoint e) {
Assert.that(e.isUDPHostCache());
if (udpHostsSet.contains(e))
return false;
// note that we do not do any comparisons to ensure that
// this host is "better" than existing hosts.
// the rationale is that we'll only ever be adding hosts
// who have a failure count of 0 (unless we're reading
// from gnutella.net, in which case all will be added),
// and we always want to try new people.
// if we've exceeded the maximum size, remove the worst element.
if(udpHosts.size() >= PERMANENT_SIZE) {
Object removed = udpHosts.remove(udpHosts.size() - 1);
udpHostsSet.remove(removed);
if(LOG.isTraceEnabled())
LOG.trace("Ejected: " + removed);
}
// just insert. we'll sort later.
udpHosts.add(e);
udpHostsSet.add(e);
dirty = true;
writeDirty = true;
return true;
}
/**
* Notification that all stored UDP host caches have been added.
* If none are stored, we load a list of defaults.
*/
public synchronized void hostCachesAdded() {
if(udpHostsSet.isEmpty())
loadDefaults();
}
protected void loadDefaults() {
// ADD DEFAULT UDP HOST CACHES HERE.
}
/**
* Creates and adds a host/port as a UDP host cache.
*/
private void createAndAdd(String host, int port) {
try {
ExtendedEndpoint ep =
new ExtendedEndpoint(host, port).setUDPHostCache(true);
add(ep);
} catch(IllegalArgumentException ignored) {}
}
/**
* Listener that listens for message from the specified hosts,
* marking any hosts that did not have a message processed
* as failed host caches, causing them to increment a failure
* count. If hosts exceed the maximum failures, they are
* removed as potential hostcaches.
*/
private class HostExpirer implements MessageListener {
private final Set hosts = new IpPortSet();
// allHosts contains all the hosts, so that we can
// iterate over successful caches too.
private final Set allHosts;
private byte[] guid;
/**
* Constructs a new HostExpirer for the specified hosts.
*/
public HostExpirer(Collection hostsToAdd) {
hosts.addAll(hostsToAdd);
allHosts = new HashSet(hostsToAdd);
removeDuplicates(hostsToAdd, hosts);
}
/**
* Removes any hosts that exist in 'all' but not in 'some'.
*/
private void removeDuplicates(Collection all, Collection some) {
// Iterate through what's in our collection vs whats in our set.
// If any entries exist in the collection but not in the set,
// then that means they resolved to the same address.
// Automatically eject entries that resolve to the same address.
Set duplicates = new HashSet(all);
duplicates.removeAll(some); // remove any hosts we're keeping.
for(Iterator i = duplicates.iterator(); i.hasNext(); ) {
ExtendedEndpoint ep = (ExtendedEndpoint)i.next();
if(LOG.isDebugEnabled())
LOG.debug("Removing duplicate entry: " + ep);
remove(ep);
}
}
/**
* Notification that a message has been processed.
*/
public void processMessage(Message m, ReplyHandler handler) {
// allow only udp replies.
if(handler instanceof UDPReplyHandler) {
if(hosts.remove(handler)) {
if(LOG.isTraceEnabled())
LOG.trace("Recieved: " + m);
}
// OPTIMIZATION: if we've gotten succesful responses from
// each hosts, unregister ourselves early.
if(hosts.isEmpty())
RouterService.getMessageRouter().
unregisterMessageListener(guid, this);
}
}
/**
* Notification that this listener is now registered with the
* specified GUID.
*/
public void registered(byte[] g) {
this.guid = g;
}
/**
* Notification that this listener is now unregistered for the
* specified guid.
*/
public void unregistered(byte[] g) {
synchronized(UDPHostCache.this) {
// Record the failures...
for(Iterator i = hosts.iterator(); i.hasNext(); ) {
ExtendedEndpoint ep = (ExtendedEndpoint)i.next();
if(LOG.isTraceEnabled())
LOG.trace("No response from cache: " + ep);
ep.recordUDPHostCacheFailure();
dirty = true;
writeDirty = true;
if(ep.getUDPHostCacheFailures() > MAXIMUM_FAILURES)
remove(ep);
}
// Then record the successes...
allHosts.removeAll(hosts);
for(Iterator i = allHosts.iterator(); i.hasNext(); ) {
ExtendedEndpoint ep = (ExtendedEndpoint)i.next();
if(LOG.isTraceEnabled())
LOG.trace("Valid response from cache: " + ep);
ep.recordUDPHostCacheSuccess();
dirty = true;
writeDirty = true;
}
}
}
}
/**
* The only FailureComparator we'll ever need.
*/
private static final Comparator FAILURE_COMPARATOR = new FailureComparator();
private static class FailureComparator implements Comparator {
public int compare(Object a, Object b) {
ExtendedEndpoint e1 = (ExtendedEndpoint)a;
ExtendedEndpoint e2 = (ExtendedEndpoint)b;
return e1.getUDPHostCacheFailures() - e2.getUDPHostCacheFailures();
}
}
}