package net.i2p.router.peermanager;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.i2p.data.Hash;
import net.i2p.data.router.RouterInfo;
import net.i2p.data.TunnelId;
import net.i2p.data.i2np.DatabaseStoreMessage;
import net.i2p.data.i2np.DeliveryStatusMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.router.JobImpl;
import net.i2p.router.MessageSelector;
import net.i2p.router.PeerSelectionCriteria;
import net.i2p.router.ReplyJob;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelInfo;
import net.i2p.util.Log;
/**
* Grab some peers that we want to test and probe them briefly to get some
* more accurate and up to date performance data. This delegates the peer
* selection to the peer manager and tests the peer by sending it a useless
* database store message
*
* TODO - What's the point? Disable this? See also notes in PeerManager.selectPeers().
* TODO - Use something besides sending the peer's RI to itself?
*/
public class PeerTestJob extends JobImpl {
private final Log _log;
private PeerManager _manager;
private boolean _keepTesting;
private static final long DEFAULT_PEER_TEST_DELAY = 5*60*1000;
/** Creates a new instance of PeerTestJob */
public PeerTestJob(RouterContext context) {
super(context);
_log = context.logManager().getLog(PeerTestJob.class);
_keepTesting = false;
getContext().statManager().createRateStat("peer.testOK", "How long a successful test takes", "Peers", new long[] { 60*1000, 10*60*1000 });
getContext().statManager().createRateStat("peer.testTooSlow", "How long a too-slow (yet successful) test takes", "Peers", new long[] { 60*1000, 10*60*1000 });
getContext().statManager().createRateStat("peer.testTimeout", "How often a test times out without a reply", "Peers", new long[] { 60*1000, 10*60*1000 });
}
/** how long should we wait before firing off new tests? */
private long getPeerTestDelay() { return DEFAULT_PEER_TEST_DELAY; }
/** how long to give each peer before marking them as unresponsive? */
private int getTestTimeout() { return 30*1000; }
/** number of peers to test each round */
private int getTestConcurrency() { return 1; }
// FIXME Exporting non-public type through public API FIXME
public synchronized void startTesting(PeerManager manager) {
_manager = manager;
_keepTesting = true;
this.getTiming().setStartAfter(getContext().clock().now() + DEFAULT_PEER_TEST_DELAY);
getContext().jobQueue().addJob(this);
if (_log.shouldLog(Log.INFO))
_log.info("Start testing peers");
}
public synchronized void stopTesting() {
_keepTesting = false;
if (_log.shouldLog(Log.INFO))
_log.info("Stop testing peers");
}
public String getName() { return "Peer test start"; }
public void runJob() {
if (!_keepTesting) return;
Set<RouterInfo> peers = selectPeersToTest();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Testing " + peers.size() + " peers");
for (RouterInfo peer : peers) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Testing peer " + peer.getIdentity().getHash().toBase64());
testPeer(peer);
}
requeue(getPeerTestDelay());
}
/**
* Retrieve a group of 0 or more peers that we want to test.
* Returned list will not include ourselves.
*
* @return set of RouterInfo structures
*/
private Set<RouterInfo> selectPeersToTest() {
PeerSelectionCriteria criteria = new PeerSelectionCriteria();
criteria.setMinimumRequired(getTestConcurrency());
criteria.setMaximumRequired(getTestConcurrency());
criteria.setPurpose(PeerSelectionCriteria.PURPOSE_TEST);
List<Hash> peerHashes = _manager.selectPeers(criteria);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Peer selection found " + peerHashes.size() + " peers");
Set<RouterInfo> peers = new HashSet<RouterInfo>(peerHashes.size());
for (Hash peer : peerHashes) {
RouterInfo peerInfo = getContext().netDb().lookupRouterInfoLocally(peer);
if (peerInfo != null) {
peers.add(peerInfo);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Test peer " + peer.toBase64() + " had no local routerInfo?");
}
}
return peers;
}
/**
* Fire off the necessary jobs and messages to test the given peer
* The message is a store of the peer's RI to itself,
* with a reply token.
*/
private void testPeer(RouterInfo peer) {
TunnelInfo inTunnel = getInboundTunnelId();
if (inTunnel == null) {
_log.warn("No tunnels to get peer test replies through!");
return;
}
TunnelId inTunnelId = inTunnel.getReceiveTunnelId(0);
RouterInfo inGateway = getContext().netDb().lookupRouterInfoLocally(inTunnel.getPeer(0));
if (inGateway == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("We can't find the gateway to our inbound tunnel?! Impossible?");
return;
}
int timeoutMs = getTestTimeout();
long expiration = getContext().clock().now() + timeoutMs;
long nonce = 1 + getContext().random().nextLong(I2NPMessage.MAX_ID_VALUE - 1);
DatabaseStoreMessage msg = buildMessage(peer, inTunnelId, inGateway.getIdentity().getHash(), nonce, expiration);
TunnelInfo outTunnel = getOutboundTunnelId();
if (outTunnel == null) {
_log.warn("No tunnels to send search out through! Something is wrong...");
return;
}
TunnelId outTunnelId = outTunnel.getSendTunnelId(0);
if (_log.shouldLog(Log.DEBUG))
_log.debug(getJobId() + ": Sending peer test to " + peer.getIdentity().getHash().toBase64()
+ " out " + outTunnel + " w/ replies through " + inTunnel);
ReplySelector sel = new ReplySelector(peer.getIdentity().getHash(), nonce, expiration);
PeerReplyFoundJob reply = new PeerReplyFoundJob(getContext(), peer, inTunnel, outTunnel);
PeerReplyTimeoutJob timeoutJob = new PeerReplyTimeoutJob(getContext(), peer, inTunnel, outTunnel, sel);
getContext().messageRegistry().registerPending(sel, reply, timeoutJob);
getContext().tunnelDispatcher().dispatchOutbound(msg, outTunnelId, null, peer.getIdentity().getHash());
}
/**
* what tunnel will we send the test out through?
*
* @return tunnel id (or null if none are found)
*/
private TunnelInfo getOutboundTunnelId() {
return getContext().tunnelManager().selectOutboundTunnel();
}
/**
* what tunnel will we get replies through?
*
* @return tunnel id (or null if none are found)
*/
private TunnelInfo getInboundTunnelId() {
return getContext().tunnelManager().selectInboundTunnel();
}
/**
* Build a message to test the peer with.
* The message is a store of the peer's RI to itself,
* with a reply token.
*/
private DatabaseStoreMessage buildMessage(RouterInfo peer, TunnelId replyTunnel, Hash replyGateway, long nonce, long expiration) {
DatabaseStoreMessage msg = new DatabaseStoreMessage(getContext());
msg.setEntry(peer);
msg.setReplyGateway(replyGateway);
msg.setReplyTunnel(replyTunnel);
msg.setReplyToken(nonce);
msg.setMessageExpiration(expiration);
return msg;
}
/**
* Simple selector looking for a dbStore of the peer specified
*
*/
private class ReplySelector implements MessageSelector {
private long _expiration;
private long _nonce;
private Hash _peer;
private boolean _matchFound;
public ReplySelector(Hash peer, long nonce, long expiration) {
_nonce = nonce;
_expiration = expiration;
_peer = peer;
_matchFound = false;
}
public boolean continueMatching() { return false; }
public long getExpiration() { return _expiration; }
public boolean isMatch(I2NPMessage message) {
if (message instanceof DeliveryStatusMessage) {
DeliveryStatusMessage msg = (DeliveryStatusMessage)message;
if (_nonce == msg.getMessageId()) {
long timeLeft = _expiration - getContext().clock().now();
if (timeLeft < 0) {
if (_log.shouldLog(Log.WARN))
_log.warn("Took too long to get a reply from peer " + _peer.toBase64()
+ ": " + (0-timeLeft) + "ms too slow");
getContext().statManager().addRateData("peer.testTooSlow", 0-timeLeft);
} else {
getContext().statManager().addRateData("peer.testOK", getTestTimeout() - timeLeft);
}
_matchFound = true;
return true;
}
}
return false;
}
public boolean matchFound() { return _matchFound; }
@Override
public String toString() {
StringBuilder buf = new StringBuilder(64);
buf.append("Test peer ").append(_peer.toBase64().substring(0,4));
buf.append(" with nonce ").append(_nonce);
return buf.toString();
}
}
/**
* Called when the peer's response is found
*/
private class PeerReplyFoundJob extends JobImpl implements ReplyJob {
private RouterInfo _peer;
private long _testBegin;
private TunnelInfo _replyTunnel;
private TunnelInfo _sendTunnel;
public PeerReplyFoundJob(RouterContext context, RouterInfo peer, TunnelInfo replyTunnel, TunnelInfo sendTunnel) {
super(context);
_peer = peer;
_replyTunnel = replyTunnel;
_sendTunnel = sendTunnel;
_testBegin = context.clock().now();
}
public String getName() { return "Peer test successful"; }
public void runJob() {
long responseTime = getContext().clock().now() - _testBegin;
if (_log.shouldLog(Log.DEBUG))
_log.debug("successful peer test after " + responseTime + " for "
+ _peer.getIdentity().getHash().toBase64() + " using outbound tunnel "
+ _sendTunnel + " and inbound tunnel "
+ _replyTunnel);
getContext().profileManager().dbLookupSuccessful(_peer.getIdentity().getHash(), responseTime);
// we know the tunnels are working
_sendTunnel.testSuccessful((int)responseTime);
_replyTunnel.testSuccessful((int)responseTime);
}
public void setMessage(I2NPMessage message) {
// noop
}
}
/**
* Called when the peer's response times out
*/
private class PeerReplyTimeoutJob extends JobImpl {
private RouterInfo _peer;
private TunnelInfo _replyTunnel;
private TunnelInfo _sendTunnel;
private ReplySelector _selector;
public PeerReplyTimeoutJob(RouterContext context, RouterInfo peer, TunnelInfo replyTunnel, TunnelInfo sendTunnel, ReplySelector sel) {
super(context);
_peer = peer;
_replyTunnel = replyTunnel;
_sendTunnel = sendTunnel;
_selector = sel;
}
public String getName() { return "Peer test failed"; }
private boolean getShouldFailPeer() { return true; }
public void runJob() {
if (_selector.matchFound())
return;
if (getShouldFailPeer())
getContext().profileManager().dbLookupFailed(_peer.getIdentity().getHash());
if (_log.shouldLog(Log.DEBUG))
_log.debug("failed peer test for "
+ _peer.getIdentity().getHash().toBase64() + " using outbound tunnel "
+ _sendTunnel + " and inbound tunnel "
+ _replyTunnel);
// don't fail the tunnels, as the peer might just plain be down, or
// otherwise overloaded
getContext().statManager().addRateData("peer.testTimeout", 1);
}
}
}