package com.limegroup.gnutella.auth; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.messages.vendor.ContentRequest; import com.limegroup.gnutella.messages.vendor.ContentResponse; import com.limegroup.gnutella.settings.ContentSettings; import com.limegroup.gnutella.util.ManagedThread; /** * Keeps track of content requests & responses. */ public class ContentManager { private static final Log LOG = LogFactory.getLog(ContentManager.class); /** Map of SHA1 to Observers listening for responses to the SHA1. */ private final Map /* URN -> List (of Responder) */ OBSERVERS = Collections.synchronizedMap(new HashMap()); /** List of Responder's that are currently waiting, in order of timeout. */ private final List RESPONDERS = new ArrayList(); /** Set or URNs that we've already requested. */ private final Set REQUESTED = Collections.synchronizedSet(new HashSet()); /** Set of URNs that have failed requesting. */ private final Set TIMEOUTS = Collections.synchronizedSet(new HashSet()); /* * LOCKING OF THE ABOVE: * * OBSERVERS may NOT be locked if RESPONDERS, TIMEOUTS or REQUESTED is locked. * RESPONDERS may NOT be locked if TIMEOUTS or REQUESTED is locked. * TIMEOUTS may be locked at any time. * REQUESTED may be locked at any time. * * In other words, locking order goes: * synchronized(OBSERVERS) { * ... * synchronized(RESPONDERS) { * ... * synchronized(TIMEOUTS) { ... } * synchronized(REQUESTED) { ... } * } * } */ /** The ContentCache. */ private final ContentCache CACHE = new ContentCache(); /** The content authority. */ private volatile ContentAuthority authority = null; /** Wehther or not we're shutting down. */ private volatile boolean shutdown = false; /** * Initializes this content manager. */ public void initialize() { CACHE.initialize(); startProcessingThread(); } /** * Shuts down this ContentManager. */ public void shutdown() { shutdown = true; CACHE.writeToDisk(); } /** Sets the content authority. */ public void setContentAuthority(ContentAuthority authority) { this.authority = authority; } /** * Determines if we've already tried sending a request & waited the time * for a response for the given URN. */ public boolean isVerified(URN urn) { return !ContentSettings.isManagementActive() || CACHE.hasResponseFor(urn) || TIMEOUTS.contains(urn); } /** * Determines if the given URN is valid. * * @param urn * @param observer * @param timeout */ public void request(URN urn, ContentResponseObserver observer, long timeout) { ContentResponseData response = CACHE.getResponse(urn); if(response != null || !ContentSettings.isManagementActive()) { if(LOG.isDebugEnabled()) LOG.debug("Immediate response for URN: " + urn); observer.handleResponse(urn, response); } else { if(LOG.isDebugEnabled()) LOG.debug("Scheduling request for URN: " + urn); scheduleRequest(urn, observer, timeout); } } /** * Does a request, blocking until a response is given or the request times out. */ public ContentResponseData request(URN urn, long timeout) { Validator validator = new Validator(); synchronized(validator) { request(urn, validator, timeout); if (validator.hasResponse()) { return validator.getResponse(); } else { try { validator.wait(); // notified when response comes in. } catch(InterruptedException ix) { LOG.warn("Interrupted while waiting for response", ix); } return validator.getResponse(); } } } /** * Gets a response if one exists. */ public ContentResponseData getResponse(URN urn) { return CACHE.getResponse(urn); } /** * Schedules a request for the given URN, timing out in the given timeout. * * @param urn * @param observer * @param timeout */ protected void scheduleRequest(URN urn, ContentResponseObserver observer, long timeout) { long now = System.currentTimeMillis(); addResponder(new Responder(now, timeout, observer, urn)); // only send if we haven't already requested. if (REQUESTED.add(urn) && authority != null) { if(LOG.isDebugEnabled()) LOG.debug("Sending request for URN: " + urn + " to authority: " + authority); authority.send(new ContentRequest(urn)); } else if(LOG.isDebugEnabled()) LOG.debug("Not sending request. No authority or already requested."); } /** * Notification that a ContentResponse was given. */ public void handleContentResponse(ContentResponse responseMsg) { URN urn = responseMsg.getURN(); // Only process if we requested this msg. // (Don't allow arbitrary responses to be processed) if(urn != null && REQUESTED.remove(urn)) { ContentResponseData response = new ContentResponseData(responseMsg); CACHE.addResponse(urn, response); if(LOG.isDebugEnabled()) LOG.debug("Adding response (" + response + ") for URN: " + urn); Collection responders = (Collection)OBSERVERS.remove(urn); if(responders != null) { removeResponders(responders); for(Iterator i = responders.iterator(); i.hasNext(); ) { Responder next = (Responder)i.next(); next.observer.handleResponse(next.urn, response); } } } else if(LOG.isWarnEnabled()) { if(urn == null) { LOG.debug("No URN in response: " + responseMsg); } else { LOG.debug("Didn't request URN: " + urn + ", msg: " + responseMsg); } } } /** * Removes all responders from RESPONDERS. */ protected void removeResponders(Collection responders) { int size = responders.size(); int removed = 0; synchronized(RESPONDERS) { for(int i = RESPONDERS.size() - 1; i >= 0; i--) { Responder next = (Responder)RESPONDERS.get(i); if(responders.contains(next)) { RESPONDERS.remove(i); removed++; } // optimization: stop early. if(removed == size) break; } } if(removed != size) LOG.warn("unable to remove all responders"); } /** * Adds a given responder into the list of responders that need to be told * when a response comes in. * * @param responder */ protected void addResponder(Responder responder) { synchronized(OBSERVERS) { Set observers = (Set)OBSERVERS.get(responder.urn); if(observers == null) observers = new HashSet(); observers.add(responder); OBSERVERS.put(responder.urn, observers); if(responder.dead != 0) addForTimeout(responder); } } /** Adds a responder into the correct place in the sorted Responders list. */ protected void addForTimeout(Responder responder) { synchronized (RESPONDERS) { if (RESPONDERS.isEmpty()) { RESPONDERS.add(responder); } else if (responder.dead <= ((Responder) RESPONDERS.get(RESPONDERS.size() - 1)).dead) { RESPONDERS.add(responder); } else { // Quick lookup. int insertion = Collections.binarySearch(RESPONDERS, responder); if (insertion < 0) insertion = (insertion + 1) * -1; RESPONDERS.add(insertion, responder); } } } /** Times out old responders. */ protected void timeout(long now) { List responders = null; synchronized(RESPONDERS) { Responder next = null; for(int i = RESPONDERS.size() - 1; i >= 0; i--) { next = (Responder)RESPONDERS.get(i); if(next.dead <= now) { REQUESTED.remove(next.urn); TIMEOUTS.add(next.urn); if(responders == null) responders = new ArrayList(2); responders.add(next); RESPONDERS.remove(i); next = null; } else { break; } } } // Now call outside of lock. if (responders != null) { for (int i = 0; i < responders.size(); i++) { Responder next = (Responder) responders.get(i); if (LOG.isDebugEnabled()) LOG.debug("Timing out responder: " + next + " for URN: " + next.urn); try { next.observer.handleResponse(next.urn, null); } catch (Throwable t) { ErrorService.error(t, "Content ContentResponseData Error"); } } } } /** * Starts the thread that does the timeout stuff & sets the content authority. * The content authority is attempted to be set here instead of outside this * thread because looking up the DNS name can block. */ protected void startProcessingThread() { Thread timeouter = new ManagedThread(new Runnable() { public void run() { // if no existing authority, try and make one. if(authority == null) setDefaultContentAuthority(); while(true) { if(shutdown) return; try { try { Thread.sleep(1000); } catch(InterruptedException ix) {} if(!shutdown) timeout(System.currentTimeMillis()); } catch(Throwable t) { ErrorService.error(t); } } } }, "ContentProcessor"); timeouter.setDaemon(true); timeouter.start(); } /** * Gets the default content authority. */ protected ContentAuthority getDefaultContentAuthority() { return new SettingsBasedContentAuthority(); } /** Sets the content authority with the default & process all pre-requested items. */ private void setDefaultContentAuthority() { ContentAuthority auth = getDefaultContentAuthority(); if(auth.initialize()) { // if we have an authority to set, grab all pre-requested items, // set the authority (so newly requested ones will immediately send to it), // and then send off those requested. // note that the timeouts on processing older requests will be lagging slightly. if (auth != null) { Set alreadyReq = new HashSet(); synchronized(REQUESTED) { alreadyReq.addAll(REQUESTED); setContentAuthority(auth); } for(Iterator i = alreadyReq.iterator(); i.hasNext(); ) { URN urn = (URN)i.next(); if(LOG.isDebugEnabled()) LOG.debug("Sending delayed request for URN: " + urn + " to: " + auth); auth.send(new ContentRequest(urn)); } } } } /** * A simple struct to allow ResponseObservers to be timed out. */ private static class Responder implements Comparable { private final long dead; private final ContentResponseObserver observer; private final URN urn; Responder(long now, long timeout, ContentResponseObserver observer, URN urn) { if(timeout != 0) this.dead = now + timeout; else this.dead = 0; this.observer = observer; this.urn = urn; } public int compareTo(Object a) { Responder o = (Responder)a; return dead < o.dead ? 1 : dead > o.dead ? -1 : 0; } } /** A blocking ContentResponseObserver. */ private static class Validator implements ContentResponseObserver { private boolean gotResponse = false; private ContentResponseData response = null; public void handleResponse(URN urn, ContentResponseData response) { synchronized(this) { gotResponse = true; this.response = response; notify(); } } public boolean hasResponse() { return gotResponse; } public ContentResponseData getResponse() { return response; } } }