package com.limegroup.gnutella.messagehandlers;
import static com.limegroup.gnutella.MessageRouter.CLEAR_TIME;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.limewire.core.settings.MessageSettings;
import org.limewire.io.GUID;
import org.limewire.security.SecurityToken;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.limegroup.gnutella.MessageRouter;
import com.limegroup.gnutella.ReplyHandler;
import com.limegroup.gnutella.Response;
import com.limegroup.gnutella.UDPService;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.messages.vendor.LimeACKVendorMessage;
@Singleton
public class LimeACKHandler implements MessageHandler {
/**
* The lifetime of OOBs guids.
*/
private static final long TIMED_GUID_LIFETIME = 25 * 1000;
private final ScheduledExecutorService backgroundExecutor;
private final Provider<MessageRouter> messageRouter;
private final Provider<UDPService> udpService;
@Inject
public LimeACKHandler(@Named("backgroundExecutor")ScheduledExecutorService backgroundExecutor,
Provider<MessageRouter> messageRouter,
Provider<UDPService> udpService) {
this.backgroundExecutor = backgroundExecutor;
this.messageRouter = messageRouter;
this.udpService = udpService;
}
@Inject
void scheduleExpirer() {
backgroundExecutor.scheduleWithFixedDelay(new Expirer(), CLEAR_TIME, CLEAR_TIME, TimeUnit.MILLISECONDS);
}
/**
* Keeps track of QueryReplies to be sent after receiving LimeAcks (sent
* if the sink wants them). Cleared every CLEAR_TIME seconds.
* TimedGUID->QueryResponseBundle.
*/
private final Map<GUID.TimedGUID, QueryResponseBundle> _outOfBandReplies =
new Hashtable<GUID.TimedGUID, QueryResponseBundle>();
public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) {
LimeACKVendorMessage ack = (LimeACKVendorMessage)msg;
GUID.TimedGUID refGUID = new GUID.TimedGUID(new GUID(ack.getGUID()),
TIMED_GUID_LIFETIME);
QueryResponseBundle bundle = _outOfBandReplies.remove(refGUID);
// token is null for old oob messages, it will just be ignored then
SecurityToken securityToken = ack.getSecurityToken();
if ((bundle != null) && (ack.getNumResults() > 0)) {
final InetAddress iaddr = addr.getAddress();
final int port = addr.getPort();
// convert responses to QueryReplies, but only send as many as the
// node wants
Iterable<QueryReply> iterable;
if (ack.getNumResults() < bundle.responses.length) {
// TODO move selection to responseToQueryReplies methods for randomization
Response[] desired = new Response[ack.getNumResults()];
System.arraycopy(bundle.responses, 0, desired, 0, desired.length);
iterable = messageRouter.get().responsesToQueryReplies(desired, bundle._query, 1, securityToken);
} else {
iterable = messageRouter.get().responsesToQueryReplies(bundle.responses,
bundle._query, 1, securityToken);
}
// send the query replies
int i = 0;
for(final QueryReply queryReply : iterable) {
backgroundExecutor.schedule(new Runnable() {
public void run () {
udpService.get().send(queryReply, iaddr, port);
}
}, (i++) * 200, TimeUnit.MILLISECONDS);
}
}
}
/** Stores (for a limited time) the resps for later out-of-band delivery -
* interacts with handleLimeACKMessage.
* @return true if the operation failed, false if not (i.e. too busy)
*/
public boolean bufferResponsesForLaterDelivery(QueryRequest query,
Response...responses) {
// store responses by guid for later retrieval
synchronized (_outOfBandReplies) {
if (_outOfBandReplies.size() < MessageSettings.MAX_BUFFERED_OOB_REPLIES.getValue()) {
GUID.TimedGUID tGUID =
new GUID.TimedGUID(new GUID(query.getGUID()),
TIMED_GUID_LIFETIME);
_outOfBandReplies.put(tGUID, new QueryResponseBundle(query,
responses));
return true;
}
return false;
}
}
private static class QueryResponseBundle {
public final QueryRequest _query;
public final Response[] responses;
public QueryResponseBundle(QueryRequest query, Response...responses) {
_query = query;
this.responses = responses;
}
}
/** Can be run to invalidate out-of-band ACKs that we are waiting for....
*/
private class Expirer implements Runnable {
public void run() {
Set<GUID.TimedGUID> toRemove = new HashSet<GUID.TimedGUID>();
synchronized (_outOfBandReplies) {
long now = System.currentTimeMillis();
for(GUID.TimedGUID currQB : _outOfBandReplies.keySet()) {
if ((currQB != null) && (currQB.shouldExpire(now)))
toRemove.add(currQB);
}
// done iterating through _outOfBandReplies, remove the
// keys now...
for(GUID.TimedGUID next : toRemove)
_outOfBandReplies.remove(next);
}
}
}
}