package com.limegroup.gnutella.bootstrap;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.collection.Cancellable;
import org.limewire.collection.FixedSizeExpiringSet;
import org.limewire.io.NetworkInstanceUtils;
import org.limewire.net.address.StrictIpPortSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.limegroup.gnutella.ConnectionServices;
import com.limegroup.gnutella.ExtendedEndpoint;
import com.limegroup.gnutella.MessageListener;
import com.limegroup.gnutella.MessageRouter;
import com.limegroup.gnutella.ReplyHandler;
import com.limegroup.gnutella.UDPReplyHandler;
import com.limegroup.gnutella.UniqueHostPinger;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PingRequest;
import com.limegroup.gnutella.messages.PingRequestFactory;
/**
* Manages a set of UDP host caches and retrieves hosts from them.
*/
@Singleton
class UDPHostCacheImpl implements UDPHostCache {
private static final Log LOG = LogFactory.getLog(UDPHostCacheImpl.class);
/**
* The maximum number of failures to allow for a given UHC.
*/
private static final int MAXIMUM_FAILURES = 5;
/**
* The maximum number of UHCs to remember between
* launches, or at any given time.
*/
public static final int PERMANENT_SIZE = 100;
/**
* The number of UHCs we try to fetch from at once.
*/
public static final int FETCH_AMOUNT = 5;
/**
* How many milliseconds to wait before retrying a UHC.
* FIXME: this is too long, we will have given up before retrying
*/
public static final int EXPIRY_TIME = 10 * 60 * 1000;
/**
* A list of UHCs, to allow easy sorting & randomizing.
* 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<ExtendedEndpoint> udpHosts = new ArrayList<ExtendedEndpoint>(PERMANENT_SIZE);
private final Set<ExtendedEndpoint> udpHostsSet = new HashSet<ExtendedEndpoint>();
private final UniqueHostPinger pinger;
/**
* A set of UHCs we've recently contacted, so we don't contact them again.
*/
private final Set<ExtendedEndpoint> attemptedHosts;
/**
* Whether or not we need to resort the UHCs by failures.
*/
private boolean dirty = false;
/**
* Whether or not the set contains data different than when we last wrote.
*/
private boolean writeDirty = false;
private final Provider<MessageRouter> messageRouter;
private final PingRequestFactory pingRequestFactory;
private final ConnectionServices connectionServices;
private final NetworkInstanceUtils networkInstanceUtils;
/**
* Constructs a new UDPHostCacheImpl that remembers attempting UHCs for the
* default expiry time.
*/
@Inject
protected UDPHostCacheImpl(UniqueHostPinger pinger,
Provider<MessageRouter> messageRouter,
PingRequestFactory pingRequestFactory,
ConnectionServices connectionServices,
NetworkInstanceUtils networkInstanceUtils) {
this(EXPIRY_TIME, pinger, messageRouter, pingRequestFactory,
connectionServices, networkInstanceUtils);
}
/**
* Constructs a new UDPHostCacheImpl that remembers attempting UHCs for the
* given amount of time, in msecs.
*
* @param connectionServices
*/
UDPHostCacheImpl(int expiryTime, UniqueHostPinger pinger,
Provider<MessageRouter> messageRouter,
PingRequestFactory pingRequestFactory,
ConnectionServices connectionServices,
NetworkInstanceUtils networkInstanceUtils) {
this.connectionServices = connectionServices;
attemptedHosts = new FixedSizeExpiringSet<ExtendedEndpoint>(PERMANENT_SIZE, expiryTime);
this.pinger = pinger;
this.messageRouter = messageRouter;
this.pingRequestFactory = pingRequestFactory;
this.networkInstanceUtils = networkInstanceUtils;
}
/**
* Writes the set of UHCs to the given stream.
*/
@Override
public synchronized void write(Writer out) throws IOException {
for(ExtendedEndpoint e: udpHosts) {
e.write(out);
}
writeDirty = false;
}
/**
* Returns true if the set of UHCs needs to be saved.
*/
@Override
public synchronized boolean isWriteDirty() {
return writeDirty;
}
/**
* Returns the number of UHCs in the set.
*/
@Override
public synchronized int getSize() {
return udpHostsSet.size();
}
/**
* Attempts to contact some UHCs to retrieve hosts. This method blocks
* while resolving hostnames.
*/
@Override
public synchronized boolean fetchHosts() {
// If the hosts have been used, shuffle and sort them
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.
LOG.trace("Shuffling and sorting UHCs");
Collections.shuffle(udpHosts);
Collections.sort(udpHosts, FAILURE_COMPARATOR);
dirty = false;
}
// Keep only the first FETCH_AMOUNT of the valid hosts.
List<ExtendedEndpoint> validHosts = new ArrayList<ExtendedEndpoint>(Math.min(FETCH_AMOUNT, udpHosts.size()));
List<ExtendedEndpoint> invalidHosts = new LinkedList<ExtendedEndpoint>();
for(ExtendedEndpoint next : udpHosts) {
if(validHosts.size() >= FETCH_AMOUNT)
break;
if(attemptedHosts.contains(next)) {
if(LOG.isTraceEnabled())
LOG.trace("Already attempted " + next);
continue;
}
// Resolve addresses and remove UHCs with invalid addresses
if(!networkInstanceUtils.isValidExternalIpPort(next)) {
if(LOG.isInfoEnabled())
LOG.info("Invalid address for " + next);
invalidHosts.add(next);
continue;
}
validHosts.add(next);
}
// Remove all invalid hosts.
for(ExtendedEndpoint next : invalidHosts) {
remove(next);
}
attemptedHosts.addAll(validHosts);
return fetch(validHosts);
}
/**
* Attempts to contact the given set of UHCs to retrieve hosts.
* Protected for testing.
*/
protected boolean fetch(Collection<? extends ExtendedEndpoint> hosts) {
if(hosts.isEmpty()) {
LOG.info("No UHCs to try");
return false;
}
if(LOG.isInfoEnabled())
LOG.info("Pinging UHCs " + hosts);
pinger.rank(
hosts,
new HostExpirer(hosts),
// cancel when connected -- don't send out any more pings
new Cancellable() {
public boolean isCancelled() {
return connectionServices.isConnected();
}
},
getPing()
);
return true;
}
/**
* Constructs and returns a ping to be sent to UHCs.
* Protected for testing.
*/
protected PingRequest getPing() {
return pingRequestFactory.createUHCPing();
}
/**
* Removes a UHC from the set, returning true if it was removed.
* Protected for testing.
*/
protected synchronized boolean remove(ExtendedEndpoint e) {
if(LOG.isInfoEnabled())
LOG.info("Removing UHC " + e);
boolean removed1=udpHosts.remove(e);
boolean removed2=udpHostsSet.remove(e);
assert removed1==removed2 : "Set "+removed1+" but queue "+removed2;
if(removed1)
writeDirty = true;
return removed1;
}
/**
* Adds a new UHC to the set, returning true if it was added.
*/
@Override
public synchronized boolean add(ExtendedEndpoint e) {
assert e.isUDPHostCache();
if(udpHostsSet.contains(e)) {
if(LOG.isTraceEnabled())
LOG.trace("Not adding known UHC " + 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(LOG.isInfoEnabled())
LOG.info("Adding UHC " + e);
// 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 UHC " + removed);
}
// just insert. we'll sort later.
udpHosts.add(e);
udpHostsSet.add(e);
dirty = true;
writeDirty = true;
return true;
}
/**
* Creates a UHC from the given host and port and adds it to the set.
*/
@SuppressWarnings("unused")
private void createAndAdd(String host, int port) {
try {
// Resolve hostnames later
add(new ExtendedEndpoint(host, port, false).setUDPHostCache(true));
} catch(IllegalArgumentException ignored) {}
}
/**
* Listener that listens for message from the specified UHCs, incrementing
* the failure counts of any that do not respond and resetting the failure
* counts of any that do. If a UHC exceeds the maximum failure count it is
* removed.
*/
private class HostExpirer implements MessageListener {
private final Set<ExtendedEndpoint> hosts = new StrictIpPortSet<ExtendedEndpoint>();
// allHosts contains all the hosts, so that we can
// iterate over successful caches too.
private final Set<ExtendedEndpoint> allHosts;
private byte[] guid;
/**
* Constructs a new HostExpirer for the specified UHCs.
*/
public HostExpirer(Collection<? extends ExtendedEndpoint> hostsToAdd) {
hosts.addAll(hostsToAdd);
allHosts = new HashSet<ExtendedEndpoint>(hostsToAdd);
removeDuplicates(hostsToAdd, hosts);
}
/**
* Removes any UHCs that exist in 'all' but not in 'some'.
*/
private void removeDuplicates(Collection<? extends ExtendedEndpoint> all, Collection<? extends ExtendedEndpoint> 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<ExtendedEndpoint> duplicates = new HashSet<ExtendedEndpoint>(all);
duplicates.removeAll(some); // remove any hosts we're keeping.
for(ExtendedEndpoint ep : duplicates) {
if(LOG.isTraceEnabled())
LOG.trace("Removing duplicate entry " + ep);
remove(ep);
}
}
/**
* Notification that a message has been processed.
*/
@Override
public void processMessage(Message m, ReplyHandler handler) {
// We expect only UDP replies
if(handler instanceof UDPReplyHandler) {
if(hosts.remove(handler)) {
if(LOG.isTraceEnabled())
LOG.trace("Recieved: " + m);
}
// OPTIMIZATION: if we've got successful responses from
// all the UHCs, unregister ourselves early
if(hosts.isEmpty()) {
LOG.trace("Unregistering message listener");
messageRouter.get().unregisterMessageListener(guid, this);
}
}
}
/**
* Notification that this listener is now registered with the
* specified GUID.
*/
@Override
public void registered(byte[] g) {
this.guid = g;
}
/**
* Notification that this listener is now unregistered for the
* specified guid.
*/
@Override
public void unregistered(byte[] g) {
synchronized(UDPHostCacheImpl.this) {
// Record the failures...
for(ExtendedEndpoint ep : hosts) {
if(LOG.isInfoEnabled())
LOG.info("No response from UHC " + ep);
ep.recordUDPHostCacheFailure();
dirty = true;
writeDirty = true;
if(ep.getUDPHostCacheFailures() > MAXIMUM_FAILURES)
remove(ep);
}
// Then record the successes...
allHosts.removeAll(hosts);
for(ExtendedEndpoint ep : allHosts) {
if(LOG.isInfoEnabled())
LOG.info("Valid response from UHC " + ep);
ep.recordUDPHostCacheSuccess();
dirty = true;
writeDirty = true;
}
}
}
}
/**
* The only FailureComparator we'll ever need.
*/
private static final Comparator<ExtendedEndpoint> FAILURE_COMPARATOR = new FailureComparator();
private static class FailureComparator implements Comparator<ExtendedEndpoint> {
public int compare(ExtendedEndpoint e1, ExtendedEndpoint e2) {
return e1.getUDPHostCacheFailures() - e2.getUDPHostCacheFailures();
}
}
}