package com.limegroup.gnutella.dht; import java.net.SocketAddress; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.concurrent.ExecutorsHelper; import org.limewire.core.settings.DHTSettings; import org.limewire.inject.EagerSingleton; import org.limewire.io.IpPort; import org.limewire.lifecycle.Service; import org.limewire.mojito.EntityKey; import org.limewire.mojito.KUID; import org.limewire.mojito.MojitoDHT; import org.limewire.mojito.concurrent.DHTFuture; import org.limewire.mojito.db.DHTValue; import org.limewire.mojito.result.FindValueResult; import org.limewire.mojito.result.StoreResult; import org.limewire.mojito.routing.Contact; import org.limewire.mojito.routing.Vendor; import org.limewire.mojito.routing.Version; import org.limewire.mojito.settings.ContextSettings; import org.limewire.util.DebugRunnable; import com.google.inject.Inject; import com.google.inject.name.Named; import com.limegroup.gnutella.connection.ConnectionLifecycleEvent; import com.limegroup.gnutella.messages.vendor.DHTContactsMessage; /** * This DHT manager starts either an active or a passive DHT controller. * It also handles switching from one mode to the other. * <p> * This class offloads blocking operations to a thread pool * so that it never blocks on critical threads such as MessageDispatcher. */ @EagerSingleton public class DHTManagerImpl implements DHTManager, Service { private static final Log LOG = LogFactory.getLog(DHTManagerImpl.class); /** * The Vendor code of this DHT Node. */ private final Vendor vendor = ContextSettings.getVendor(); /** * The Version of this DHT Node. */ private final Version version = ContextSettings.getVersion(); /** * The DHTController instance. */ private DHTController controller = new NullDHTController(); /** * List of event listeners for ConnectionLifeCycleEvents. */ private final List<DHTEventListener> dhtEventListeners = new ArrayList<DHTEventListener>(1); /** * The executor to use to execute blocking DHT methods, such * as stopping or starting a Mojito instance (which perform * network and disk I/O). * */ private final Executor executor; /** * The executor to use for dispatching events. */ private final Executor dispatchExecutor; private volatile boolean enabled = true; private final DHTControllerFactory dhtControllerFactory; /** * Constructs the DHTManager, using the given Executor to invoke blocking * methods. The executor MUST be single-threaded, otherwise there will be * failures. * * @param service executor for executing blocking DHT methods * @param dhtControllerFactory creates DHT node controllers */ @Inject public DHTManagerImpl(@Named("dhtExecutor") Executor service, DHTControllerFactory dhtControllerFactory) { this.executor = service; this.dispatchExecutor = ExecutorsHelper.newProcessingQueue("DHT-EventDispatch"); this.dhtControllerFactory = dhtControllerFactory; } @Inject void register(org.limewire.lifecycle.ServiceRegistry registry) { registry.register(this); } public String getServiceName() { return org.limewire.i18n.I18nMarker.marktr("Mojito DHT"); } public void initialize() { } public void start() { } /* * (non-Javadoc) * @see com.limegroup.gnutella.dht.DHTManager#setEnabled(boolean) */ public void setEnabled(boolean enabled) { this.enabled = enabled; } /* * (non-Javadoc) * @see com.limegroup.gnutella.dht.DHTManager#isEnabled() */ public boolean isEnabled() { if (!DHTSettings.DISABLE_DHT_NETWORK.getValue() && !DHTSettings.DISABLE_DHT_USER.getValue() && enabled) { return true; } return false; } /* * (non-Javadoc) * @see com.limegroup.gnutella.dht.DHTManager#start(com.limegroup.gnutella.dht.DHTManager.DHTMode) */ public synchronized void start(DHTMode mode) { executor.execute(createSwitchModeCommand(mode)); } /* * (non-Javadoc) * @see com.limegroup.gnutella.dht.DHTManager#stop() */ public synchronized void stop() { Runnable command = new DebugRunnable(new Runnable() { public void run() { synchronized (DHTManagerImpl.this) { try { createSwitchModeCommand(DHTMode.INACTIVE).run(); } finally { DHTManagerImpl.this.notifyAll(); } } } }); executor.execute(command); try { this.wait(10000); } catch (InterruptedException err) { LOG.error("InterruptedException", err); } } /** * Creates and returns a Runnable that switches the DHT node from * the current <code>DHTMode</code> to the given <code>mode</code>. * * @param mode the new mode of the DHT node * @return Runnable that switches the mode */ private Runnable createSwitchModeCommand(final DHTMode mode) { Runnable command = new DebugRunnable(new Runnable() { public void run() { synchronized (DHTManagerImpl.this) { // Controller already running in the current mode? if (controller.getDHTMode() == mode) { return; } controller.stop(); if (mode == DHTMode.ACTIVE) { controller = dhtControllerFactory.createActiveDHTNodeController( vendor, version, DHTManagerImpl.this); } else if (mode == DHTMode.PASSIVE) { controller = dhtControllerFactory .createPassiveDHTNodeController(vendor, version, DHTManagerImpl.this); } else if (mode == DHTMode.PASSIVE_LEAF) { controller = dhtControllerFactory.createPassiveLeafController( vendor, version, DHTManagerImpl.this); } else { controller = new NullDHTController(); } controller.start(); } } }); return command; } public void addActiveDHTNode(final SocketAddress hostAddress) { executor.execute(new Runnable() { public void run() { synchronized(DHTManagerImpl.this) { controller.addActiveDHTNode(hostAddress); } } }); } public void addPassiveDHTNode(final SocketAddress hostAddress) { executor.execute(new Runnable() { public void run() { synchronized(DHTManagerImpl.this) { controller.addPassiveDHTNode(hostAddress); } } }); } public void addressChanged() { // Do this in a different thread as there are some blocking //disk and network ops. executor.execute(new DebugRunnable(new Runnable() { public void run() { synchronized(DHTManagerImpl.this) { if (controller.isRunning()) { controller.stop(); controller.start(); } } } })); } public synchronized List<IpPort> getActiveDHTNodes(int maxNodes){ return controller.getActiveDHTNodes(maxNodes); } public synchronized DHTMode getDHTMode() { return controller.getDHTMode(); } public synchronized boolean isRunning() { return controller.isRunning(); } public synchronized boolean isBootstrapped() { return controller.isBootstrapped(); } public synchronized boolean isMemberOfDHT() { return isRunning() && isBootstrapped(); } public synchronized boolean isWaitingForNodes() { return controller.isWaitingForNodes(); } /** * Adds a listener to DHT Events. * <p> * Be aware that listeners will receive events after * after the DHT has dispatched them. It is possible that * the DHT's status may have changed between the time the * event was dispatched and the time the event is received * by a listener. */ public synchronized void addEventListener(DHTEventListener listener) { if(dhtEventListeners.contains(listener)) throw new IllegalArgumentException("Listener " + listener + " already registered"); dhtEventListeners.add(listener); } /** * Sends an event to all listeners. * <p> * Be aware that to prevent deadlock, listeners may receive * the event long after the DHT's status has changed, and the * current status may be very different. * <p> * No events will be received in a different order than they were * dispatched, though. */ public synchronized void dispatchEvent(final DHTEvent event) { if(!dhtEventListeners.isEmpty()) { final List<DHTEventListener> listeners = new ArrayList<DHTEventListener>(dhtEventListeners); dispatchExecutor.execute(new Runnable() { public void run() { for(DHTEventListener listener : listeners) { listener.handleDHTEvent(event); } } }); } } public synchronized void removeEventListener(DHTEventListener listener) { dhtEventListeners.remove(listener); } /** * This getter is for internal use only. The Mojito DHT is not meant to * be handled or passed around independently, as only the DHT controllers * know how to interact correctly with it. */ public synchronized MojitoDHT getMojitoDHT() { return controller.getMojitoDHT(); } /** * Shuts the DHT down if we got disconnected from the network. * The nodeAssigner will take care of restarting this DHT node if * it still qualifies. * <p> * If this event is not related to disconnection from the network, it * is forwarded to the controller for proper handling. */ public void handleConnectionLifecycleEvent(final ConnectionLifecycleEvent evt) { Runnable command = null; if (evt.isDisconnectedEvent() || evt.isNoInternetEvent()) { command = new DebugRunnable( new Runnable() { public void run() { synchronized(DHTManagerImpl.this) { if (controller.isRunning() && !DHTSettings.FORCE_DHT_CONNECT.getValue()) { controller.stop(); controller = new NullDHTController(); } } } }); } else { command = new Runnable() { public void run() { synchronized(DHTManagerImpl.this) { controller.handleConnectionLifecycleEvent(evt); } } }; } executor.execute(command); } public Vendor getVendor() { return vendor; } public Version getVersion() { return version; } public void handleDHTContactsMessage(final DHTContactsMessage msg) { executor.execute(new Runnable() { public void run() { synchronized(DHTManagerImpl.this) { for (Contact node : msg.getContacts()) { controller.addContact(node); } } } }); } /** * Calls the {@link MojitoDHT#put} if a bootstrappable DHT is available. * Also handles the locking properly to ensure thread safety. * * @param eKey the entity key used to perform lookup in the DHT. * * @return an instance of <code>DHTFuture</code> containing the result of the lookup. * <br> Returns null if DHT is unavailable or the DHT is not bootstrapped. * */ public synchronized DHTFuture<FindValueResult> get(EntityKey eKey) { MojitoDHT mojitoDHT = getMojitoDHT(); if (LOG.isDebugEnabled()) LOG.debug("DHT:" + mojitoDHT); if (mojitoDHT == null || !mojitoDHT.isBootstrapped()) { LOG.debug("DHT is null or is not bootstrapped"); return null; } // instantiated here so it can record its instantiation time DHTFuture<FindValueResult> future = mojitoDHT.get(eKey); return future; } /** * Calls the {@link MojitoDHT#put} if a bootstrappable DHT is available. * Also handles the locking properly to ensure thread safety. * * @param key a unique id used as a key to find the associated value. * @param value the value which will be stored in the DHT. * * @return an instance of <code>DHTFuture</code> containing the result of the storage. * <br> Returns null if DHT is unavailable or the DHT is not bootstrapped. */ public synchronized DHTFuture<StoreResult> put(KUID key, DHTValue value) { MojitoDHT mojitoDHT = getMojitoDHT(); if (LOG.isDebugEnabled()) LOG.debug("DHT: " + mojitoDHT); if (mojitoDHT == null || !mojitoDHT.isBootstrapped()) { LOG.debug("DHT is null or unable to bootstrap"); return null; } DHTFuture<StoreResult> future = mojitoDHT.put(key, value); return future; } }