package com.limegroup.gnutella.guess; import java.net.InetAddress; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.RouterService; import com.limegroup.gnutella.UDPService; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.messages.PingReply; import com.limegroup.gnutella.messages.PingRequest; import com.limegroup.gnutella.messages.QueryRequest; /** Utility class for sending GUESS queries. */ public class OnDemandUnicaster { private static final int CLEAR_TIME = 5 * 60 * 1000; // 5 minutes /** GUESSEndpoints => QueryKey. */ private static final Map _queryKeys; /** Access to UDP traffic. */ private static final UDPService _udp; /** Short term store for queries waiting for query keys. * GUESSEndpoints => URNs */ private static final Map _bufferedURNs; static { // static initializers are only called once, right? _queryKeys = new Hashtable(); // need sychronization _bufferedURNs = new Hashtable(); // synchronization handy _udp = UDPService.instance(); // schedule a runner to clear various data structures RouterService.schedule(new Expirer(), CLEAR_TIME, CLEAR_TIME); } /** Feed me QueryKey pongs so I can query people.... * pre: pr.getQueryKey() != null */ public static void handleQueryKeyPong(PingReply pr) throws NullPointerException, IllegalArgumentException { // validity checks // ------ if (pr == null) throw new NullPointerException("null pong"); QueryKey qk = pr.getQueryKey(); if (qk == null) throw new IllegalArgumentException("no key in pong"); // ------ // create guess endpoint // ------ InetAddress address = pr.getInetAddress(); int port = pr.getPort(); GUESSEndpoint endpoint = new GUESSEndpoint(address, port); // ------ // store query key _queryKeys.put(endpoint, qk); // if a buffered query exists, send it... // ----- SendLaterBundle bundle = (SendLaterBundle) _bufferedURNs.remove(endpoint); if (bundle != null) { QueryRequest query = QueryRequest.createQueryKeyQuery(bundle._queryURN, qk); RouterService.getMessageRouter().originateQueryGUID(query.getGUID()); _udp.send(query, endpoint.getAddress(), endpoint.getPort()); } // ----- } /** Sends out a UDP query with the specified URN to the specified host. * @throws IllegalArgumentException if ep or queryURN are null. * @param ep the location you want to query. * @param queryURN the URN you are querying for. */ public static void query(GUESSEndpoint ep, URN queryURN) throws IllegalArgumentException { // validity checks // ------ if (ep == null) throw new IllegalArgumentException("No Endpoint!"); if (queryURN == null) throw new IllegalArgumentException("No urn to look for!"); // ------ // see if you have a QueryKey - if not, request one // ------ QueryKey key = (QueryKey) _queryKeys.get(ep); if (key == null) { GUESSEndpoint endpoint = new GUESSEndpoint(ep.getAddress(), ep.getPort()); SendLaterBundle bundle = new SendLaterBundle(queryURN); _bufferedURNs.put(endpoint, bundle); PingRequest pr = PingRequest.createQueryKeyRequest(); _udp.send(pr, ep.getAddress(), ep.getPort()); } // ------ // if possible send query, else buffer // ------ else { QueryRequest query = QueryRequest.createQueryKeyQuery(queryURN, key); RouterService.getMessageRouter().originateQueryGUID(query.getGUID()); _udp.send(query, ep.getAddress(), ep.getPort()); } // ------ } private static class SendLaterBundle { private static final int MAX_LIFETIME = 60 * 1000; public final URN _queryURN; private final long _creationTime; public SendLaterBundle(URN urn) { _queryURN = urn; _creationTime = System.currentTimeMillis(); } public boolean shouldExpire() { return ((System.currentTimeMillis() - _creationTime) > MAX_LIFETIME); } } /** @return true if the Query Key data structure was cleared. * @param lastQueryKeyClearTime The last time query keys were cleared. * @param queryKeyClearInterval how often you like query keys to be * cleared. * This method has been disaggregated from the Expirer class for ease of * testing. */ private static boolean clearDataStructures(long lastQueryKeyClearTime, long queryKeyClearInterval) throws Throwable { boolean clearedQueryKeys = false; // Clear the QueryKeys if needed // ------ if ((System.currentTimeMillis() - lastQueryKeyClearTime) > queryKeyClearInterval) { clearedQueryKeys = true; // we just indiscriminately clear all the query keys - we // could just expire 'old' ones, but the benefit is marginal _queryKeys.clear(); } // ------ // Get rid of all the buffered URNs that should be expired // ------ synchronized (_bufferedURNs) { Iterator iter = _bufferedURNs.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); SendLaterBundle bundle = (SendLaterBundle) entry.getValue(); if (bundle.shouldExpire()) iter.remove(); } } // ------ return clearedQueryKeys; } /** This is run to clear various data structures used. * Made package access for easy test access. */ private static class Expirer implements Runnable { // 24 hours private static final int QUERY_KEY_CLEAR_TIME = 24 * 60 * 60 * 1000; private long _lastQueryKeyClearTime; public Expirer() { _lastQueryKeyClearTime = System.currentTimeMillis(); } public void run() { try { if (clearDataStructures(_lastQueryKeyClearTime, QUERY_KEY_CLEAR_TIME)) _lastQueryKeyClearTime = System.currentTimeMillis(); } catch(Throwable t) { ErrorService.error(t); } } } }