package net.i2p.router.message;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.data.Hash;
import net.i2p.data.Lease;
import net.i2p.data.LeaseSet;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelInfo;
import net.i2p.util.SimpleTimer;
/**
* Helper for OCMOSJ
*
* This is the place where we make I2P go fast.
*
* We have five static caches.
* - The LeaseSet cache is used to decide whether to bundle our own leaseset,
* which minimizes overhead.
* - The Lease cache is used to persistently send to the same lease for the destination,
* which keeps the streaming lib happy by minimizing out-of-order delivery.
* - The Tunnel and BackloggedTunnel caches are used to persistently use the same outbound tunnel
* for the same destination,
* which keeps the streaming lib happy by minimizing out-of-order delivery.
* - The last reply requested cache ensures that a reply is requested every so often,
* so that failed tunnels are recognized.
*
* @since 0.9 moved out of OCMOSJ
*/
public class OutboundCache {
/**
* Use the same outbound tunnel as we did for the same destination previously,
* if possible, to keep the streaming lib happy
* Use two caches - although a cache of a list of tunnels per dest might be
* more elegant.
* Key the caches on the source+dest pair.
*
* NOT concurrent.
*/
final Map<HashPair, TunnelInfo> tunnelCache = new HashMap<HashPair, TunnelInfo>(64);
/*
* NOT concurrent.
*/
final Map<HashPair, TunnelInfo> backloggedTunnelCache = new HashMap<HashPair, TunnelInfo>(64);
/**
* Returns the reply lease set if forced to do so,
* or if configured to do so,
* or if a certain percentage of the time if configured to do so,
* or if our lease set has changed since we last talked to them,
* or 10% of the time anyway so they don't forget us (disabled for now),
* or null otherwise.
*
* Note that wantACK randomly forces us another 5% of the time.
*
* We don't want to do this too often as a typical 2-lease leaseset
* in a DatabaseStoreMessage is 861+37=898 bytes -
* when added to garlic it's a 1056-byte addition total, which is huge.
*
* Key the cache on the source+dest pair.
*
* Concurrent.
*/
final Map<HashPair, LeaseSet> leaseSetCache = new ConcurrentHashMap<HashPair, LeaseSet>(64);
/**
* Use the same inbound tunnel (i.e. lease) as we did for the same destination previously,
* if possible, to keep the streaming lib happy
* Key the caches on the source+dest pair.
*
* We're going to use the lease until it expires, as long as it remains in the current leaseSet.
*
* If not found,
* fetch the next lease that we should try sending through, randomly chosen
* from within the sorted leaseSet (NOT sorted by # of failures through each
* lease).
*
* Concurrent.
*/
final ConcurrentHashMap<HashPair, Lease> leaseCache = new ConcurrentHashMap<HashPair, Lease>(64);
/**
* This cache is used to ensure that we request a reply every so often.
* Hopefully this allows the router to recognize a failed tunnel and switch,
* before upper layers like streaming lib fail, even for low-bandwidth
* connections like IRC.
*
* Concurrent.
*/
final Map<HashPair, Long> lastReplyRequestCache = new ConcurrentHashMap<HashPair, Long>(64);
private final RouterContext _context;
private static final int CLEAN_INTERVAL = 5*60*1000;
public OutboundCache(RouterContext ctx) {
_context = ctx;
_context.simpleTimer2().addPeriodicEvent(new OCMOSJCacheCleaner(), CLEAN_INTERVAL, CLEAN_INTERVAL);
}
/**
* Key used to cache things with based on source + dest
* @since 0.8.3
*/
static class HashPair {
private final Hash sh, dh;
public HashPair(Hash s, Hash d) {
sh = s;
dh = d;
}
public int hashCode() {
return sh.hashCode() ^ dh.hashCode();
}
public boolean equals(Object o) {
if (o == null || !(o instanceof HashPair))
return false;
HashPair hp = (HashPair) o;
return sh.equals(hp.sh) && dh.equals(hp.dh);
}
}
/**
* Called on failure to give us a better chance of success next time.
* Of course this is probably 60s too late.
* And we could pick the bad ones at random again.
* Or remove entries that were sent and succeeded after this was sent but before this failed.
* But it's a start.
*
* @param lease may be null
* @param inTunnel may be null
* @param outTunnel may be null
*/
void clearCaches(HashPair hashPair, Lease lease, TunnelInfo inTunnel, TunnelInfo outTunnel) {
if (inTunnel != null) { // if we wanted an ack, we sent our lease too
leaseSetCache.remove(hashPair);
}
if (lease != null) {
// remove only if still equal to lease (concurrent)
leaseCache.remove(hashPair, lease);
}
if (outTunnel != null) {
synchronized(tunnelCache) {
TunnelInfo t = backloggedTunnelCache.get(hashPair);
if (t != null && t.equals(outTunnel))
backloggedTunnelCache.remove(hashPair);
t = tunnelCache.get(hashPair);
if (t != null && t.equals(outTunnel))
tunnelCache.remove(hashPair);
}
}
}
/**
* @since 0.8.8
*/
public void clearAllCaches() {
leaseSetCache.clear();
leaseCache.clear();
synchronized(tunnelCache) {
backloggedTunnelCache.clear();
tunnelCache.clear();
}
lastReplyRequestCache.clear();
}
/**
* Clean out old leaseSets
*/
private static void cleanLeaseSetCache(RouterContext ctx, Map<HashPair, LeaseSet> tc) {
long now = ctx.clock().now();
for (Iterator<LeaseSet> iter = tc.values().iterator(); iter.hasNext(); ) {
LeaseSet l = iter.next();
if (l.getEarliestLeaseDate() < now)
iter.remove();
}
}
/**
* Clean out old leases
*/
private static void cleanLeaseCache(Map<HashPair, Lease> tc) {
for (Iterator<Lease> iter = tc.values().iterator(); iter.hasNext(); ) {
Lease l = iter.next();
if (l.isExpired(Router.CLOCK_FUDGE_FACTOR))
iter.remove();
}
}
/**
* Clean out old tunnels
* Caller must synchronize on tc.
*/
private static void cleanTunnelCache(RouterContext ctx, Map<HashPair, TunnelInfo> tc) {
for (Iterator<Map.Entry<HashPair, TunnelInfo>> iter = tc.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry<HashPair, TunnelInfo> entry = iter.next();
HashPair k = entry.getKey();
TunnelInfo tunnel = entry.getValue();
// This is a little sneaky, but get the _from back out of the "opaque" hash key
if (!ctx.tunnelManager().isValidTunnel(k.sh, tunnel))
iter.remove();
}
}
/**
* Clean out old reply times
*/
private static void cleanReplyCache(RouterContext ctx, Map<HashPair, Long> tc) {
long now = ctx.clock().now();
for (Iterator<Long> iter = tc.values().iterator(); iter.hasNext(); ) {
Long l = iter.next();
if (l.longValue() < now - CLEAN_INTERVAL)
iter.remove();
}
}
private class OCMOSJCacheCleaner implements SimpleTimer.TimedEvent {
public void timeReached() {
cleanLeaseSetCache(_context, leaseSetCache);
cleanLeaseCache(leaseCache);
synchronized(tunnelCache) {
cleanTunnelCache(_context, tunnelCache);
cleanTunnelCache(_context, backloggedTunnelCache);
}
cleanReplyCache(_context, lastReplyRequestCache);
}
}
}