package com.limegroup.gnutella.search; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.GUID; import com.limegroup.gnutella.ReplyHandler; import com.limegroup.gnutella.util.ProcessingQueue; /** * Manages dynamic querying for Ultrapeers. * * This maintains the data for all active queries for this Ultrapeer and any * of its leaves, also providing an interface for removing active queries. * Queries may be removed, for example, when a leaf node with an active query * disconnects from the Ultrapeer. */ public final class QueryDispatcher implements Runnable { /** * <tt>Map</tt> of outstanding queries. */ private final Map QUERIES = new HashMap(); // GUID -> QueryHandler /** * <tt>List</tt> of new queries to add. * LOCKING: Thread-safe, although you must obtain a lock on NEW_QUERIES if * it's ever iterated over. */ private final List NEW_QUERIES = Collections.synchronizedList(new LinkedList()); /** * <tt>QueryDispatcher</tt> instance following singleton. */ private static final QueryDispatcher INSTANCE = new QueryDispatcher(); /** * The ProcessingQueue that handles sending queries out. * * Items are added to this only if it's not already processing anything. */ private final ProcessingQueue PROCESSOR = new ProcessingQueue("QueryDispatcher"); /** * Whether or not processing is already active. If it is, we don't start it up again * when adding new queries. */ private boolean _active; /** * Instance accessor for the <tt>QueryDispatcher</tt>. * * @return the <tt>QueryDispatcher</tt> instance */ public static QueryDispatcher instance() { return INSTANCE; } /** * Creates a new <tt>QueryDispatcher</tt> instance. */ private QueryDispatcher() {} /** * Adds the specified <tt>QueryHandler</tt> to the list of queries to * process. * * @param handler the <tt>QueryHandler</tt> instance to add */ public void addQuery(QueryHandler handler) { handler.sendQuery(); // immediately send out one query. synchronized(NEW_QUERIES) { NEW_QUERIES.add(handler); if(NEW_QUERIES.size() == 1 && !_active) { _active = true; PROCESSOR.add(this); } } } /** * This method removes all queries for the given <tt>ReplyHandler</tt> * instance. * * @param handler the handler that should have it's queries removed */ public void removeReplyHandler(ReplyHandler handler) { // if it's not a leaf connection, we don't care that it's closed if(!handler.isSupernodeClientConnection()) return; remove(handler); } /** Updates the relevant QueryHandler with result stats from the leaf. */ public void updateLeafResultsForQuery(GUID queryGUID, int numResults) { synchronized (QUERIES) { QueryHandler qh = (QueryHandler) QUERIES.get(queryGUID); if (qh != null) qh.updateLeafResults(numResults); } } /** Gets the number of results the Leaf has reported so far. * @return a non-negative number if the guid exists, else -1. */ public int getLeafResultsForQuery(GUID queryGUID) { synchronized (QUERIES) { QueryHandler qh = (QueryHandler) QUERIES.get(queryGUID); if (qh == null) return -1; else return qh.getNumResultsReportedByLeaf(); } } /** * Removes all queries using the specified <tt>ReplyHandler</tt> * from NEW_QUERIES & QUERIES. * * @param handler the <tt>ReplyHandler</tt> to remove */ private void remove(ReplyHandler handler) { synchronized(NEW_QUERIES) { Iterator iter = NEW_QUERIES.iterator(); while(iter.hasNext()) { QueryHandler qh = (QueryHandler)iter.next(); if(qh.getReplyHandler() == handler) iter.remove(); } } synchronized(QUERIES) { Iterator iter = QUERIES.values().iterator(); while(iter.hasNext()) { QueryHandler qh = (QueryHandler)iter.next(); if(qh.getReplyHandler() == handler) iter.remove(); } } } /** * Removes the specified <tt>ReplyHandler</tt> from NEW_QUERIES & QUERIES. * * @param handler the <tt>ReplyHandler</tt> to remove */ private void remove(GUID guid) { synchronized(NEW_QUERIES) { Iterator iter = NEW_QUERIES.iterator(); while(iter.hasNext()) { QueryHandler qh = (QueryHandler)iter.next(); if(qh.getGUID().equals(guid)) iter.remove(); } } synchronized(QUERIES) { Iterator iter = QUERIES.values().iterator(); while(iter.hasNext()) { QueryHandler qh = (QueryHandler)iter.next(); if(qh.getGUID().equals(guid)) iter.remove(); } } } /** * Processes queries until there is nothing left to process, * or there are no new queries to process. */ public void run() { while(true) { try { Thread.sleep(400); } catch(InterruptedException ignored) {} try { // If there are no more queries to process... if(!processQueries()) { synchronized(NEW_QUERIES) { // If there are no new queries to add, // set active to false & leave. if(NEW_QUERIES.isEmpty()) { _active = false; return; } // else, loop. } } // else, loop. } catch(Throwable t) { ErrorService.error(t); } } } /** * Processes current queries. */ private boolean processQueries() { synchronized(NEW_QUERIES) { synchronized(QUERIES) { Iterator iter = NEW_QUERIES.iterator(); while (iter.hasNext()) { QueryHandler qh = (QueryHandler) iter.next(); QUERIES.put(qh.getGUID(), qh); } } NEW_QUERIES.clear(); } synchronized(QUERIES) { Iterator iter = QUERIES.values().iterator(); while(iter.hasNext()) { QueryHandler handler = (QueryHandler)iter.next(); handler.sendQuery(); if(handler.hasEnoughResults()) iter.remove(); } return !QUERIES.isEmpty(); } } /** * Removes all queries that match this GUID. * * @param g the <tt>GUID</tt> of the search to remove */ public void addToRemove(GUID g) { remove(g); } }