package com.limegroup.gnutella.downloader; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.core.settings.DHTSettings; import org.limewire.mojito.settings.LookupSettings; import org.limewire.nio.observer.Shutdownable; import com.limegroup.gnutella.ConnectionServices; import com.limegroup.gnutella.DownloadManager; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.altlocs.AlternateLocation; import com.limegroup.gnutella.dht.DHTEvent; import com.limegroup.gnutella.dht.DHTEventListener; import com.limegroup.gnutella.dht.DHTManager; import com.limegroup.gnutella.dht.db.AltLocFinder; import com.limegroup.gnutella.dht.db.SearchListener; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.util.LimeWireUtils; /** * A manager for controlling how requeries are sent in downloads. * This class has specific functionality that differs for * Basic & PRO. * <p> * The manager keeps track of what queries have been sent out, * when queries can begin, and how long queries should wait for results. * </p> * <p> * It is controlled through {@link #sendQuery()}, {@link #activate()} and * {@link #cleanUp()}. * </p> */ class RequeryManager implements DHTEventListener { private static final Log LOG = LogFactory.getLog(RequeryManager.class); /** The types of requeries that can be currently active. */ public static enum QueryType { DHT, GNUTELLA }; /** * The time to wait between requeries, in milliseconds. This time can * safely be quite small because it is overridden by the global limit in * DownloadManager. Package-access and non-final for testing. * * @see com.limegroup.gnutella.DownloadManager#TIME_BETWEEN_GNUTELLA_REQUERIES */ static long TIME_BETWEEN_REQUERIES = 3L * 60L * 1000L; //3 minutes private final RequeryListener requeryListener; private final DownloadManager downloadManager; private final AltLocFinder altLocFinder; private final DHTManager dhtManager; /** The type of the last query this sent out. */ private volatile QueryType lastQueryType; /** The number of DHT queries already done for this downloader. */ private volatile int numDHTQueries; /** The time the last query of either type was sent out. */ private volatile long lastQuerySent; /** True if a gnutella query has been sent. */ protected volatile boolean sentGnutellaQuery; /** True if requerying has been activated by the user. */ protected volatile boolean activated; /** * a dht lookup to Shutdownable if we get shut down * not null if a DHT query is currently out (and not finished, success or failure) */ private volatile Shutdownable dhtQuery; private final ConnectionServices connectionServices; /** * Used to be notified of finished dht queries. * * Package access for tests. */ final AltLocSearchHandler searchHandler = new AltLocSearchHandler(); RequeryManager(RequeryListener requeryListener, DownloadManager manager, AltLocFinder finder, DHTManager dhtManager, ConnectionServices connectionServices) { this.requeryListener = requeryListener; this.downloadManager = manager; this.altLocFinder = finder; this.dhtManager = dhtManager; this.connectionServices = connectionServices; dhtManager.addEventListener(this); } /** Returns true if we're currently waiting for any kinds of results. */ boolean isWaitingForResults() { if(lastQueryType == null) return false; switch(lastQueryType) { case DHT: return dhtQuery != null && getTimeLeftInQuery() > 0; case GNUTELLA: return getTimeLeftInQuery() > 0; } return false; } /** Returns the kind of last requery we sent. */ QueryType getLastQueryType() { return lastQueryType; } /** Returns how much time, in milliseconds, is left in the current query. */ long getTimeLeftInQuery() { return TIME_BETWEEN_REQUERIES - (System.currentTimeMillis() - lastQuerySent); } /** Sends a requery, if allowed. */ void sendQuery() { if(canSendQueryNow()) { if(canSendDHTQueryNow()) sendDHTQuery(); else if(!sentGnutellaQuery) sendGnutellaQuery(); else LOG.debug("Can send a query now, but not sending it?!"); } else { LOG.debug("Tried to send query, but cannot do it now."); } } /** True if a requery can immediately be performed or can be triggered from a user action. */ boolean canSendQueryAfterActivate() { return !sentGnutellaQuery || canSendDHTQueryNow(); } /** Returns true if a query can be sent right now. */ boolean canSendQueryNow() { // PRO users can always send the DHT query, but only Gnutella after activate. if(LimeWireUtils.isPro()) return canSendDHTQueryNow() || (activated && canSendQueryAfterActivate()); else return activated && canSendQueryAfterActivate(); } /** Allows activated queries to begin. */ void activate() { activated = true; } /** Removes all event listeners, cancels ongoing searches and cleans up references. */ void cleanUp() { Shutdownable f = dhtQuery; dhtQuery = null; if (f != null) f.shutdown(); dhtManager.removeEventListener(this); } /** Specifically, this cancels the DHT query if the DHT is stopped. */ public void handleDHTEvent(DHTEvent evt) { if (evt.getType() == DHTEvent.Type.STOPPED) { handleAltLocSearchDone(false); numDHTQueries = 0; } } void handleAltLocSearchDone(boolean success){ dhtQuery = null; // This changes the state to GAVE_UP regardless of success, // because even if this was a success (it found results), // it's possible the download isn't going to want to use // those results. requeryListener.lookupFinished(QueryType.DHT); } /** * @return true if the dht is up and can be used for altloc queries. */ private boolean isDHTUp() { return DHTSettings.ENABLE_DHT_ALT_LOC_QUERIES.getValue() && dhtManager.isMemberOfDHT(); } /** True if another DHT query can be sent right now. */ private boolean canSendDHTQueryNow() { if (!isDHTUp()) return false; return numDHTQueries == 0 || (numDHTQueries < DHTSettings.MAX_DHT_ALT_LOC_QUERY_ATTEMPTS.getValue() && System.currentTimeMillis() - lastQuerySent >= DHTSettings.TIME_BETWEEN_DHT_ALT_LOC_QUERIES.getValue() ); } private void sendDHTQuery() { LOG.debug("Sending a DHT requery!"); lastQuerySent = System.currentTimeMillis(); lastQueryType = QueryType.DHT; numDHTQueries++; requeryListener.lookupStarted(QueryType.DHT, Math.max(TIME_BETWEEN_REQUERIES, LookupSettings.FIND_VALUE_LOOKUP_TIMEOUT.getValue())); URN sha1Urn = requeryListener.getSHA1Urn(); if (sha1Urn != null) { dhtQuery = altLocFinder.findAltLocs(sha1Urn, searchHandler); } // fail silently otherwise as before } /** Sends a Gnutella Query */ private void sendGnutellaQuery() { // If we don't have stable connections, wait until we do. if (hasStableConnections()) { QueryRequest qr = requeryListener.createQuery(); if(qr != null) { downloadManager.sendQuery(qr); LOG.debug("Sent a gnutella requery!"); sentGnutellaQuery = true; lastQueryType = QueryType.GNUTELLA; lastQuerySent = System.currentTimeMillis(); requeryListener.lookupStarted(QueryType.GNUTELLA, TIME_BETWEEN_REQUERIES); } else { sentGnutellaQuery = true; requeryListener.lookupFinished(QueryType.GNUTELLA); } } else { LOG.debug("Tried to send a gnutella requery, but no stable connections."); requeryListener.lookupPending(QueryType.GNUTELLA, CONNECTING_WAIT_TIME); } } /** * How long we'll wait before attempting to download again after checking * for stable connections (and not seeing any) */ private static final int CONNECTING_WAIT_TIME = 750; private static final int MIN_NUM_CONNECTIONS = 2; private static final int MIN_CONNECTION_MESSAGES = 6; private static final int MIN_TOTAL_MESSAGES = 45; static boolean NO_DELAY = false; // For testing /** * Determines if we have any stable connections to send a requery down. */ private boolean hasStableConnections() { if ( NO_DELAY ) return true; // For Testing without network connection // TODO: Note that on a private network, these conditions might // be too strict. // Wait till your connections are stable enough to get the minimum // number of messages return connectionServices.countConnectionsWithNMessages(MIN_CONNECTION_MESSAGES) >= MIN_NUM_CONNECTIONS && connectionServices.getActiveConnectionMessages() >= MIN_TOTAL_MESSAGES; } private class AltLocSearchHandler implements SearchListener<AlternateLocation> { public void searchFailed() { RequeryManager.this.handleAltLocSearchDone(false); } public void handleResult(AlternateLocation alternateLocation) { // ManagedDownloader installs its own AlternatelocationListener so it // is notified of all results RequeryManager.this.handleAltLocSearchDone(true); } } }