package com.limegroup.gnutella.dht;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.collection.Cancellable;
import org.limewire.core.settings.ConnectionSettings;
import org.limewire.core.settings.DHTSettings;
import org.limewire.io.IpPort;
import org.limewire.io.IpPortImpl;
import org.limewire.io.NetworkUtils;
import com.google.inject.Provider;
import com.limegroup.gnutella.ConnectionServices;
import com.limegroup.gnutella.ExtendedEndpoint;
import com.limegroup.gnutella.HostCatcher;
import com.limegroup.gnutella.MessageListener;
import com.limegroup.gnutella.ReplyHandler;
import com.limegroup.gnutella.UDPPinger;
import com.limegroup.gnutella.UniqueHostPinger;
import com.limegroup.gnutella.dht.DHTManager.DHTMode;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PingReply;
import com.limegroup.gnutella.messages.PingRequestFactory;
/**
* Fetches DHT hosts from the Gnutella network through the use of a UDP ping.
* <p>
* Implicitly, a request for DHT hosts will also propagate the knowledge
* through the network, thanks to the <code>MessageRouter</code>'s pong
* forwarding logic.
* <p>
* First the <code>DHTNodeFetcher</code> tries to get active nodes directly from
* the {@link HostCatcher}. If that fails, it tries to send UDP pings to nodes
* who support the DHT. If that fails too, it sends UDP pings to all nodes in
* the HostCatcher.
* <p>
* <code>DHTNodeFetcher</code> uses the ranker from the <code>HostCatcher</code>,
* which it cancels when the <tt>AbstractDHTController</tt> is able to bootstrap.
* <p>
* <code>DHTNodeFetcher</code> can also start a timer task to periodically
* requests hosts until the manager is able to bootstrap.
*/
public class DHTNodeFetcher {
private static final Log LOG = LogFactory.getLog(DHTNodeFetcher.class);
/**
* The instance of the bootstrapper to which we hand back bootstrap hosts.
* Also used to cancel this node fetcher if the DHT was able to bootstrap.
*/
private final DHTBootstrapper bootstrapper;
/**
* The time of the last ping request(s) in the network.
*/
private volatile long lastRequest = 0L;
/**
* The Runnable that requests DHT hosts.
*/
private ScheduledFuture<?> fetcherTask = null;
/**
* A lock for the TimerTask.
*/
private final Object fetcherTaskLock = new Object();
/**
* Whether or not the fetcher is currently pinging a single host.
*/
private final AtomicBoolean pingingSingleHost = new AtomicBoolean(false);
/**
* A settable expiry time for the pings.
*/
private volatile int pingExpireTime = -1;
private final ConnectionServices connectionServices;
private final Provider<HostCatcher> hostCatcher;
private final ScheduledExecutorService backgroundExecutor;
private final Provider<UDPPinger> udpPinger;
private final Provider<UniqueHostPinger> uniqueHostPinger;
private final PingRequestFactory pingRequestFactory;
public DHTNodeFetcher(DHTBootstrapper bootstrapper,
ConnectionServices connectionServices,
Provider<HostCatcher> hostCatcher,
ScheduledExecutorService backgroundExecutor,
Provider<UDPPinger> udpPinger,
Provider<UniqueHostPinger> uniqueHostPinger,
PingRequestFactory pingRequestFactory) {
this.connectionServices = connectionServices;
this.hostCatcher = hostCatcher;
this.backgroundExecutor = backgroundExecutor;
this.udpPinger = udpPinger;
this.uniqueHostPinger = uniqueHostPinger;
this.bootstrapper = bootstrapper;
this.pingRequestFactory = pingRequestFactory;
}
/**
* Requests active DHT hosts from the Gnutella network. This method has to
* be synchronized because it can be called either directly by the manager
* or by the timer task.
* <p>
* This method gets hosts from the HostCatcher and therefore uses the
* UniqueHostPinger of the HostCatcher in order to avoid pinging hosts
* twice.
*/
private synchronized void requestDHTHosts() {
LOG.debug("Requesting DHT hosts");
if(!connectionServices.isConnected()) {
return;
}
//TODO: min version: for now, 0
List<ExtendedEndpoint> dhtHosts =
hostCatcher.get().getDHTSupportEndpoint(0);
//first see if we have any active dht node in our HostCatcher and add them all
//to the bootstrapper.
//The list is ordered by Active nodes first so as soon as we get an inactive
//node we can exit the loop.
boolean haveActive = false;
for(ExtendedEndpoint ep : dhtHosts) {
if (!DHTMode.ACTIVE.equals(ep.getDHTMode())) {
break;
}
if(LOG.isDebugEnabled()){
LOG.debug("Adding active host from HostCatcher: "+ ep.getInetSocketAddress());
}
haveActive = true;
bootstrapper.addBootstrapHost(ep.getInetSocketAddress());
}
if(haveActive) { //we have added active hosts already - no need to request
return;
}
//We don't have active hosts --> send UDP pings
long now = System.currentTimeMillis();
if(now - lastRequest < DHTSettings.DHT_NODE_FETCHER_TIME.getValue()) {
return;
}
if(pingingSingleHost.get()) {
return;
}
lastRequest = now;
Message m = pingRequestFactory.createUDPingWithDHTIPPRequest();
MessageListener listener = new UDPPingerRequestListener();
Cancellable canceller = new UDPPingRankerCanceller();
if(!dhtHosts.isEmpty()) {
LOG.debug("Sending ping to dht capable hosts");
//we don't have active hosts but have hosts that support dht
uniqueHostPinger.get().rank(dhtHosts, listener, canceller, m);
} else {
LOG.debug("Sending ping to all hosts");
//send to all hosts
hostCatcher.get().sendMessageToAllHosts(m,
listener, canceller);
}
}
/**
* Sends a UDP ping requesting DHT node to the specified host.
*
* @param hostAddress the <tt>SocketAddress</tt> of the host to send the ping to.
*/
public void requestDHTHosts(SocketAddress hostAddress) {
if(!connectionServices.isConnected()) {
return;
}
if(!(hostAddress instanceof InetSocketAddress)) {
return;
}
//this should preempt over ping ranker, as we know this hostAddress
//can send back DHT hosts
if(!pingingSingleHost.getAndSet(true)) {
IpPort ipp = new IpPortImpl((InetSocketAddress) hostAddress);
if(LOG.isDebugEnabled()) {
LOG.debug("Requesting DHT hosts from host " + hostAddress);
}
Message m = pingRequestFactory.createUDPingWithDHTIPPRequest();
udpPinger.get().rank(Arrays.asList(ipp),
new SinglePingRequestListener(), null, m, pingExpireTime);
}
}
/**
* Starts the DHTNodeFetcher.
*/
public void start() {
synchronized (fetcherTaskLock) {
if (fetcherTask != null) {
return;
}
long fetcherTime = DHTSettings.DHT_NODE_FETCHER_TIME.getValue();
long initialFetch = (long) (Math.random() * fetcherTime);
Runnable task = new Runnable() {
public void run() {
requestDHTHosts();
}
};
fetcherTask = backgroundExecutor.scheduleWithFixedDelay(task, initialFetch, fetcherTime, TimeUnit.MILLISECONDS);
}
}
/**
* Stops the DHTNodeFetcher.
*/
public void stop() {
synchronized (fetcherTaskLock) {
if (fetcherTask != null) {
fetcherTask.cancel(true);
fetcherTask = null;
}
}
}
/**
* Returns true if the DHTNodeFetcher is running.
*/
public boolean isRunning() {
synchronized (fetcherTaskLock) {
return fetcherTask != null;
}
}
/**
* Processes the ping reply containing DHT IP:Ports and
* hands those back to the DHT bootstrapper.
*/
private void processPingReply(Message m) {
if(!(m instanceof PingReply)) {
return;
}
if(!isRunning()) {
return;
}
PingReply reply = (PingReply) m;
Collection<IpPort> list = ConnectionSettings.FILTER_CLASS_C.getValue() ?
NetworkUtils.filterOnePerClassC(reply.getPackedDHTIPPorts()) :
reply.getPackedDHTIPPorts();
if(LOG.isDebugEnabled()) {
LOG.debug("Received ping reply from "+reply.getAddress());
}
for (IpPort ipp : list) {
bootstrapper.addBootstrapHost(new InetSocketAddress(
ipp.getInetAddress(), ipp.getPort()));
}
}
public void setPingExpireTime(int expireTime) {
pingExpireTime = expireTime;
}
/**
* This <tt>Cancellable</tt> is used to cancel UDP ping requests
* sent to sets of hosts. Cancelling is triggered by any of the following conditions:
* <ol>
* <li>We are now sending a ping to a single host.
* <li>The maximum delay has been exceeded
* <li>We are not connected to the network
* <li>We are not anymore waiting for DHT nodes to bootstrap
*
*/
private class UDPPingRankerCanceller implements Cancellable{
/** Cancels the HostCatcher pings **/
public boolean isCancelled() {
//disregard delay if this is a ping to a single host
long delay = System.currentTimeMillis() - lastRequest;
//stop when not waiting anymore OR when not connected to the Gnutella network
//OR timeout
boolean cancel = (pingingSingleHost.get()
|| delay > DHTSettings.MAX_DHT_NODE_FETCHER_TIME.getValue()
|| !connectionServices.isConnected()
|| !isRunning());
if(cancel){
if(LOG.isDebugEnabled()) {
LOG.debug("Cancelling UDP ping after "+delay+" ms, connected: "
+connectionServices.isConnected()+", waiting: "+bootstrapper.isWaitingForNodes());
}
}
return cancel;
}
}
private class UDPPingerRequestListener implements MessageListener{
/** Response to our UDP ping **/
public void processMessage(Message m, ReplyHandler handler) {
processPingReply(m);
}
public void registered(byte[] guid) {}
public void unregistered(byte[] guid) {}
}
/**
* In the case of a single ping, we have to handle the de-registration.
*/
private class SinglePingRequestListener extends UDPPingerRequestListener{
@Override
public void processMessage(Message m, ReplyHandler handler) {
super.processMessage(m, handler);
pingingSingleHost.set(false);
}
@Override
public void unregistered(byte[] guid) {
if(LOG.isDebugEnabled()) {
LOG.debug("Unregistering Ping");
}
pingingSingleHost.set(false);
}
}
}