package com.limegroup.gnutella.guess;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.GUID;
import org.limewire.io.IpPort;
import org.limewire.io.IpPortSet;
import org.limewire.lifecycle.ServiceScheduler;
import org.limewire.security.AddressSecurityToken;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.limegroup.gnutella.MessageRouter;
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.PingRequestFactory;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.messages.QueryRequestFactory;
/** Utility class for sending GUESS queries.
*/
@EagerSingleton
public class OnDemandUnicaster {
private static final Log LOG = LogFactory.getLog(OnDemandUnicaster.class);
// time to store buffered hosts waiting for a QK to query.
private static final int CLEAR_TIME = 5 * 60 * 1000; // 5 minutes
// time to store hosts we've sent a query to.
private static final int QUERIED_HOSTS_CLEAR_TIME = 30 * 1000; // 30 seconds
/** IpPorts that we've queried for this GUID. */
private final Map<GUID.TimedGUID, Set<IpPort>> _queriedHosts;
/** GUESSEndpoints => AddressSecurityToken. */
private final Map<GUESSEndpoint, AddressSecurityToken> _queryKeys;
/**
* Short term store for queries waiting for query keys.
* GUESSEndpoints => URNs
*/
private final Map<GUESSEndpoint, SendLaterBundle> _bufferedURNs;
private final QueryRequestFactory queryRequestFactory;
private final UDPService udpService;
private final Provider<MessageRouter> messageRouter;
private final PingRequestFactory pingRequestFactory;
@Inject
public OnDemandUnicaster(QueryRequestFactory queryRequestFactory,
UDPService udpService,
Provider<MessageRouter> messageRouter,
PingRequestFactory pingRequestFactory) {
this.queryRequestFactory = queryRequestFactory;
this.udpService = udpService;
this.messageRouter = messageRouter;
this.pingRequestFactory = pingRequestFactory;
// static initializers are only called once, right?
_queryKeys = new Hashtable<GUESSEndpoint, AddressSecurityToken>(); // need sychronization
_bufferedURNs = new Hashtable<GUESSEndpoint, SendLaterBundle>(); // synchronization handy
_queriedHosts = new HashMap<GUID.TimedGUID, Set<IpPort>>();
}
@Inject
public void register(ServiceScheduler serviceScheduler,
@Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor) {
// schedule a runner to clear various data structures
serviceScheduler.scheduleWithFixedDelay("OnDemandUnicaster.Expirer", new Expirer(), CLEAR_TIME, CLEAR_TIME, TimeUnit.MILLISECONDS, backgroundExecutor);
serviceScheduler.scheduleWithFixedDelay("OnDemandUnicaster.QueriedHostsExpirer", new QueriedHostsExpirer(), QUERIED_HOSTS_CLEAR_TIME, QUERIED_HOSTS_CLEAR_TIME, TimeUnit.MILLISECONDS, backgroundExecutor);
}
/** Feed me AddressSecurityToken pongs so I can query people....
* pre: pr.getQueryKey() != null
*/
public void handleQueryKeyPong(PingReply pr)
throws NullPointerException, IllegalArgumentException {
// validity checks
// ------
if (pr == null)
throw new NullPointerException("null pong");
AddressSecurityToken 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 = _bufferedURNs.remove(endpoint);
if (bundle != null)
sendQuery(bundle._queryURN, qk, endpoint);
// -----
}
/** 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 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 AddressSecurityToken - if not, request one
// ------
AddressSecurityToken key = _queryKeys.get(ep);
if (key == null) {
GUESSEndpoint endpoint = new GUESSEndpoint(ep.getInetAddress(),
ep.getPort());
SendLaterBundle bundle = new SendLaterBundle(queryURN);
_bufferedURNs.put(endpoint, bundle);
PingRequest pr = pingRequestFactory.createQueryKeyRequest();
udpService.send(pr, ep.getInetAddress(), ep.getPort());
}
// ------
// if possible send query, else buffer
// ------
else {
sendQuery(queryURN, key, ep);
}
// ------
}
/**
* Determines if the given host was sent a direct UDP URN query
* in the last 30 seconds.
*/
public boolean isHostQueriedForGUID(GUID guid, IpPort host) {
synchronized(_queriedHosts) {
Set<IpPort> hosts = _queriedHosts.get(new GUID.TimedGUID(guid));
return hosts != null ? hosts.contains(host) : false;
}
}
private void sendQuery(URN urn, AddressSecurityToken qk, IpPort ipp) {
QueryRequest query = queryRequestFactory.createQueryKeyQuery(urn, qk);
// store the query's GUID -> IPP so that when we get replies over
// UDP we can allow them without requiring the whole ReplyNumber/ACK
// thing.
GUID qGUID = new GUID(query.getGUID());
synchronized(_queriedHosts) {
GUID.TimedGUID guid = new GUID.TimedGUID(qGUID, QUERIED_HOSTS_CLEAR_TIME);
Set<IpPort> hosts = _queriedHosts.get(guid);
if(hosts == null)
hosts = new IpPortSet();
hosts.add(ipp);
// Always re-add, so the TimedGUID will last longer
_queriedHosts.put(guid, hosts);
}
if(LOG.isDebugEnabled())
LOG.debug("Sending query with GUID: " + qGUID + " for URN: " + urn + " to host: " + ipp);
messageRouter.get().originateQueryGUID(query.getGUID());
udpService.send(query, ipp.getInetAddress(), ipp.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);
}
}
/**
* This method has been disaggregated from the Expirer class for ease of
* testing.
* @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.
*/
private boolean clearDataStructures(long lastQueryKeyClearTime,
long queryKeyClearInterval) {
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) {
for(Iterator<SendLaterBundle> iter = _bufferedURNs.values().iterator(); iter.hasNext(); ) {
SendLaterBundle bundle = iter.next();
if (bundle.shouldExpire())
iter.remove();
}
}
// ------
return clearedQueryKeys;
}
/** This is run to clear various data structures used.
* Made package access for easy test access.
*/
private 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() {
if (clearDataStructures(_lastQueryKeyClearTime,
QUERY_KEY_CLEAR_TIME))
_lastQueryKeyClearTime = System.currentTimeMillis();
}
}
/** This is run to clear various data structures used.
* Made package access for easy test access.
*/
private class QueriedHostsExpirer implements Runnable {
public void run() {
synchronized(_queriedHosts) {
long now = System.currentTimeMillis();
for(Iterator<GUID.TimedGUID> iter = _queriedHosts.keySet().iterator(); iter.hasNext(); ) {
GUID.TimedGUID guid = iter.next();
if(guid.shouldExpire(now))
iter.remove();
}
}
}
}
}