package com.limegroup.gnutella;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
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.collection.Cancellable;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.io.IpPort;
import org.limewire.io.NetworkUtils;
import org.limewire.service.ErrorService;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PingRequestFactory;
/**
* Sends Gnutella messages via UDP to a set of hosts and calls back to a
* listener whenever responses are returned.
*/
public class UDPPingerImpl implements UDPPinger {
private static final Log LOG = LogFactory.getLog(UDPPingerImpl.class);
protected static final ExecutorService QUEUE = ExecutorsHelper.newProcessingQueue("UDPHostRanker");
/**
* The time to wait before expiring a message listener.
*
* Non-final for testing.
*/
public static int DEFAULT_LISTEN_EXPIRE_TIME = 20 * 1000;
/** Send pings every this often */
private static final long SEND_INTERVAL = 500;
/** Send this many pings each time */
private static final int MAX_SENDS = 15;
/**
* The current number of datagrams we've sent in the past 500 milliseconds.
*/
private static int _sentAmount;
/**
* The last time we sent a datagram.
*/
private static long _lastSentTime;
private final Provider<MessageRouter> messageRouter;
private final ScheduledExecutorService backgroundExecutor;
private final Provider<UDPService> udpService;
private final PingRequestFactory pingRequestFactory;
@Inject
public UDPPingerImpl(Provider<MessageRouter> messageRouter,
@Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor,
Provider<UDPService> udpService,
PingRequestFactory pingRequestFactory) {
this.messageRouter = messageRouter;
this.backgroundExecutor = backgroundExecutor;
this.udpService = udpService;
this.pingRequestFactory = pingRequestFactory;
}
public void rank(Collection<? extends IpPort> hosts, Cancellable canceller) {
rank(hosts, null, canceller, null);
}
public void rank(final Collection<? extends IpPort> hosts,
final MessageListener listener,
Cancellable canceller,
final Message message) {
rank(hosts, listener, canceller, message, -1);
}
public void rank(final Collection<? extends IpPort> hosts,
final MessageListener listener,
Cancellable canceller,
final Message message,
int expireTime) {
if(hosts == null)
throw new NullPointerException("null hosts not allowed");
if(canceller == null) {
canceller = new Cancellable() {
public boolean isCancelled() { return false; }
};
}
if(LOG.isDebugEnabled()) {
LOG.debug("Adding hosts "+hosts+" with message: "+message+" to processing queue");
}
SenderBundle bundle;
if(expireTime > 0) {
bundle = new SenderBundle(hosts, listener, canceller, message, expireTime);
} else {
bundle = new SenderBundle(hosts, listener, canceller, message, DEFAULT_LISTEN_EXPIRE_TIME);
}
bundle.validateHosts();
QUEUE.execute(bundle);
}
/**
* Waits for UDP listening to be activated.
*/
private boolean waitForListening(Cancellable canceller) {
int waits = 0;
while(!udpService.get().isListening() && waits < 10 &&
!canceller.isCancelled()) {
try {
Thread.sleep(600);
} catch (InterruptedException e) {
// Should never happen.
ErrorService.error(e);
}
waits++;
}
return waits < 10;
}
/**
* Sends the given send bundle.
*/
protected void send(Collection<? extends IpPort> hosts,
final MessageListener listener,
Cancellable canceller,
Message message, int expireTime) {
// something went wrong with UDPService - don't try to send
if (!waitForListening(canceller))
return;
if(message == null)
message = pingRequestFactory.createUDPPing();
final byte[] messageGUID = message.getGUID();
if (listener != null)
messageRouter.get().registerMessageListener(messageGUID, listener);
for(IpPort ipp : hosts) {
if(canceller.isCancelled())
break;
sendSingleMessage(ipp, message);
}
// also take care of any MessageListeners
if (listener != null) {
// Now schedule a runnable that will remove the mapping for the GUID
// of the above message after 20 seconds so that we don't store it
// indefinitely in memory for no reason.
Runnable udpMessagePurger = new Runnable() {
public void run() {
messageRouter.get().unregisterMessageListener(messageGUID, listener);
}
};
// Purge after 20 seconds.
backgroundExecutor.schedule(udpMessagePurger, expireTime, TimeUnit.MILLISECONDS);
}
}
protected void sendSingleMessage(IpPort host, Message message) {
long now = System.currentTimeMillis();
if(now > _lastSentTime + SEND_INTERVAL) {
_sentAmount = 0;
} else if(_sentAmount == MAX_SENDS) {
try {
Thread.sleep(SEND_INTERVAL);
now = System.currentTimeMillis();
} catch(InterruptedException ignored) {}
_sentAmount = 0;
}
if(LOG.isTraceEnabled())
LOG.trace("Sending to " + host + ": " + message.getClass()+" "+message);
udpService.get().send(message, host);
_sentAmount++;
_lastSentTime = now;
}
/**
* Simple bundle that can send itself.
*/
private class SenderBundle implements Runnable {
private final Collection<? extends IpPort> hosts;
private final MessageListener listener;
private final Cancellable canceller;
private final Message message;
private final int expireTime;
public SenderBundle(Collection<? extends IpPort> hosts, MessageListener listener,
Cancellable canceller, Message message, int expireTime) {
this.hosts = hosts;
this.listener = listener;
this.canceller = canceller;
this.message = message;
this.expireTime = expireTime;
}
public void run() {
send(hosts,listener,canceller,message,expireTime);
}
/**
* Runs through all the hosts and throws an IllegalArgumentException
* if any host is invalid.
*/
void validateHosts() {
for(IpPort host : hosts) {
// check avoids address re-resolution
if(!NetworkUtils.isValidAddressAndPort(host.getInetAddress().getAddress(), host.getPort()))
throw new IllegalArgumentException("invalid host: " + host);
}
}
}
}