package com.limegroup.gnutella.dht; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.collection.FixedSizeLIFOSet; import org.limewire.collection.FixedSizeLIFOSet.EjectionPolicy; import org.limewire.concurrent.FutureEvent; import org.limewire.core.settings.DHTSettings; import org.limewire.mojito.KUID; import org.limewire.mojito.MojitoDHT; import org.limewire.mojito.concurrent.DHTFuture; import org.limewire.mojito.concurrent.DHTFutureAdapter; import org.limewire.mojito.exceptions.DHTException; import org.limewire.mojito.result.BootstrapResult; import org.limewire.mojito.result.PingResult; import org.limewire.mojito.result.BootstrapResult.ResultType; import org.limewire.mojito.util.ExceptionUtils; import org.limewire.service.ErrorService; class DHTBootstrapperImpl implements DHTBootstrapper { private static final Log LOG = LogFactory.getLog(DHTBootstrapperImpl.class); /** * A list of DHT bootstrap hosts coming from the Gnutella network. * Limit size to 50 for now. */ private final Set<SocketAddress> hosts = new FixedSizeLIFOSet<SocketAddress>(50, EjectionPolicy.FIFO); /** * A flag that indicates whether or not we've tried to * bootstrap from the RouteTable */ private boolean triedRouteTable = false; /** * The future of the ping process */ private DHTFuture<PingResult> pingFuture; /** * A flag that indicates whether or not the current * pingFuture (see above) is pinging Nodes from the * RouteTable */ private boolean fromRouteTable = false; /** * The future of the bootstrap process */ private DHTFuture<BootstrapResult> bootstrapFuture; /** * The DHT controller */ private final DHTController controller; /** * The DHTNodeFetcher instance */ private DHTNodeFetcher nodeFetcher; /** * The lock Object */ private final Object lock = new Object(); private final DHTNodeFetcherFactory dhtNodeFetcherFactory; public DHTBootstrapperImpl(DHTController controller, DHTNodeFetcherFactory dhtNodeFetcherFactory) { this.controller = controller; this.dhtNodeFetcherFactory = dhtNodeFetcherFactory; } /** * Boostraps in the following order: * <ol> * <li>If we have received hosts from the Gnutella network, try them. * <li>Else try the persisted routing table. * <li>Else try the SIMPP list. * <li>Else start node fetcher and wait for hosts coming from the network. * </ol> * If at any moment while bootstraping from the routing table * we receive nodes from the network, it should pre-empt the existing * bootstrap and start with them. This is achieved by calling cancel on * the future. */ public void bootstrap() { synchronized (lock) { if (getMojitoDHT().isBootstrapped()) { return; } if (hosts.isEmpty()) { tryBootstrapFromRouteTable(); } else { tryBootstrapFromHostsSet(); } } } /** * Adds a host to the head of a list of bootstrap hosts ordered by Most Recently Seen. * If this bootstrapper is waiting for hosts or is bootstrapping from the persisted RT, * this method tries to bootstrap immediately after. * * @param hostAddress the SocketAddress of the new bootstrap host. */ public void addBootstrapHost(SocketAddress hostAddress) { synchronized (lock) { if (!getMojitoDHT().isRunning() || getMojitoDHT().isBootstrapped()) { return; } if(LOG.isDebugEnabled()) { LOG.debug("Adding host: "+hostAddress); } hosts.add(hostAddress); tryBootstrapFromHostsSet(); } } public void addPassiveNode(SocketAddress hostAddress) { synchronized (lock) { if (nodeFetcher == null || !isWaitingForNodes()) { return; } nodeFetcher.requestDHTHosts(hostAddress); } } /** * Stops bootstrapping */ public void stop() { synchronized (lock) { if (pingFuture != null) { pingFuture.cancel(true); pingFuture = null; } if (bootstrapFuture != null) { bootstrapFuture.cancel(true); bootstrapFuture = null; } stopNodeFetcher(); triedRouteTable = false; fromRouteTable = false; } } /** * We're waiting for Nodes if: * <ol> * <li>We're NOT bootstrapped * <li>And there's no bootstrap process active OR we are bootstrapping from routetable * </ol> */ public boolean isWaitingForNodes() { synchronized (lock) { return !getMojitoDHT().isBootstrapped() && bootstrapFuture == null; } } /** * Tries to bootstrap the local Node from an existing * RouteTable. */ private void tryBootstrapFromRouteTable() { synchronized (lock) { // Make sure we try this only once. if (triedRouteTable) { return; } if (pingFuture != null || bootstrapFuture != null) { return; } pingFuture = getMojitoDHT().findActiveContact(); pingFuture.addFutureListener(new PongListener(pingFuture)); triedRouteTable = true; fromRouteTable = true; } } /** * Tries to bootstrap the local Node from the hosts Set */ private void tryBootstrapFromHostsSet() { synchronized (lock) { // We're already in the bootstrapping stage? If so // don't bother any further! if (bootstrapFuture != null) { return; } // Are we already pinging somebody? Interrupt the // process in case of RouteTable pings as we've // probably more luck with a fresh IP:Port we got // from the DHTNodeFetcher! if (fromRouteTable) { fromRouteTable = false; if (pingFuture != null) { pingFuture.cancel(true); pingFuture = null; } } if (pingFuture != null) { return; } Iterator<SocketAddress> it = hosts.iterator(); assert (it.hasNext()); SocketAddress addr = it.next(); it.remove(); pingFuture = getMojitoDHT().ping(addr); pingFuture.addFutureListener(new PongListener(pingFuture)); } } /** * Notify our connections and event listeners * that we are now a bootstrapped DHT node */ private void finish() { controller.sendUpdatedCapabilities(); } /** * Returns MojitoDHT instance */ private MojitoDHT getMojitoDHT() { return controller.getMojitoDHT(); } /** * Stops the DHTNodeFetcher. You must synchronize on * the 'lock' Object prior to calling this! */ private void stopNodeFetcher() { if (nodeFetcher != null) { nodeFetcher.stop(); nodeFetcher = null; } } /** * Gets the SIMPP host responsible for the key space containing the local node ID. * * Non-private for testing. * * @return the SocketAddress of a SIMPP bootstrap host, or null if we don't have any. */ SocketAddress getSimppHost() { String[] simppHosts = DHTSettings.DHT_BOOTSTRAP_HOSTS.get(); List<SocketAddress> list = new ArrayList<SocketAddress>(simppHosts.length); for (String hostString : simppHosts) { int index = hostString.indexOf(":"); if(index < 0 || index == hostString.length()-1) { if (LOG.isErrorEnabled()) { LOG.error(new UnknownHostException("invalid SIMPP host: " + hostString)); } continue; } try { String host = hostString.substring(0, index); int port = Integer.parseInt(hostString.substring(index+1).trim()); list.add(new InetSocketAddress(host, port)); } catch(NumberFormatException nfe) { if (LOG.isErrorEnabled()) { LOG.error(new UnknownHostException("invalid host: " + hostString)); } } } if (list.isEmpty()) { return null; } KUID localId = getMojitoDHT().getLocalNodeID(); //each host in the list is responsible for a subspace of the keyspace //first 4 bits responsible for dividing the keyspace int localPrefix = ((localId.getBytes()[0] & 0xF0) >> 4); //now map to hostlist size int index = (int)((list.size()/16f) * localPrefix); return list.get(index); } /** For testing. */ boolean isBootstrappingFromRouteTable() { return fromRouteTable; } /** For testing. */ DHTFuture<PingResult> getPingFuture() { return pingFuture; } /** For testing */ DHTFuture<BootstrapResult> getBootstrapFuture() { return bootstrapFuture; } /** * The PongListener waits for the Ping response (Pong) from a * remote Node. If a Node responds we'll begin bootstrapping * from it and if it doesn't we'll: * <ol> * <li>start the DHTNodeFetcher if it isn't already running * <li>check the hosts Set for other Nodes and ping 'em * </ol> */ private class PongListener extends DHTFutureAdapter<PingResult> { private final DHTFuture<PingResult> myFuture; public PongListener(DHTFuture<PingResult> myFuture) { this.myFuture = myFuture; } @Override protected void operationComplete(FutureEvent<PingResult> event) { switch (event.getType()) { case SUCCESS: handleFutureSuccess(event.getResult()); break; case CANCELLED: handleCancellationException(); break; case EXCEPTION: handleExecutionException(event.getException()); break; } } private void handleFutureSuccess(PingResult result) { synchronized (lock) { if (pingFuture != myFuture) { return; } pingFuture = null; // Stop the DHTNodeFetcher, we don't need it anymore // as we found a Node that did respond to our initial // bootstrap ping stopNodeFetcher(); bootstrapFuture = getMojitoDHT().bootstrap(result.getContact()); bootstrapFuture.addFutureListener(new BootstrapListener()); } } private void handleExecutionException(ExecutionException e) { synchronized (lock) { if (pingFuture != myFuture) { return; } pingFuture = null; if (ExceptionUtils.isCausedBy(e, DHTException.class) || ExceptionUtils.isCausedBy(e, TimeoutException.class)) { // Try to bootstrap from a SIMPP Host if // bootstrapping failed // and try the hosts Set otherwise SocketAddress simpp = null; if (fromRouteTable && (simpp = getSimppHost()) != null) { addBootstrapHost(simpp); } else { retry(); } } else if (!ExceptionUtils.isCausedBy(e, IllegalArgumentException.class)) { LOG.error("ExecutionException", e); ErrorService.error(e); stop(); } } } private void retry() { // Start the DHTNodeFetcher if it isn't running // The NodeFetcher calls addBootstrapHost() which // will restart the bootstrapping. Otherwise see // if there are entries in the hosts Set if (nodeFetcher == null) { nodeFetcher = dhtNodeFetcherFactory.createNodeFetcher(DHTBootstrapperImpl.this); nodeFetcher.start(); } else { bootstrap(); } } private void handleCancellationException() { synchronized (lock) { LOG.debug("Bootstrap Ping Cancelled"); if (pingFuture != myFuture) { return; } stop(); } } } /** * The BootstrapListener waits for the result of the bootstrapping result. * On a success we'll update our capabilities and if bootstrapping failed * we'll just try it again. */ private class BootstrapListener extends DHTFutureAdapter<BootstrapResult> { @Override protected void operationComplete(FutureEvent<BootstrapResult> event) { switch (event.getType()) { case SUCCESS: handleFutureSuccess(event.getResult()); break; case CANCELLED: handleCancellationException(); break; case EXCEPTION: handleExecutionException(event.getException()); break; } } private void handleFutureSuccess(BootstrapResult result) { boolean finish = false; synchronized (lock) { bootstrapFuture = null; ResultType type = result.getResultType(); LOG.debug("Future success type: "+ type); switch(type) { case BOOTSTRAP_SUCCEEDED: finish = true; break; case BOOTSTRAP_FAILED: // Try again! bootstrap(); break; default: //ignore other results break; } } // Do not hold 'lock' when calling finish. if (finish) { finish(); } } private void handleExecutionException(ExecutionException e) { synchronized (lock) { LOG.error("ExecutionException", e); if (!(e.getCause() instanceof DHTException)) { ErrorService.error(e); } stop(); } } public void handleCancellationException() { synchronized (lock) { LOG.debug("Bootstrap Canceled"); stop(); } } } }