package com.limegroup.gnutella;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.limewire.collection.MultiIterable;
import org.limewire.core.settings.MessageSettings;
import org.limewire.inspection.Inspectable;
import org.limewire.io.GUID;
import org.limewire.util.Base32;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.messages.Message.Network;
import com.limegroup.gnutella.search.ResultCounter;
import com.limegroup.gnutella.util.ClassCNetworks;
/**
* The reply routing table. Given a GUID from a reply message header,
* this tells you where to route the reply. It is mutable mapping
* from globally unique 16-byte message IDs to connections. Old
* mappings may be purged without warning, preferably using a FIFO
* policy. This class makes a distinction between not having a mapping
* for a GUID and mapping that GUID to null (in the case of a removed
* ReplyHandler).<p>
*
* This class can also optionally keep track of the number of reply bytes
* routed per GUID. This can be useful for implementing fair flow-control
* strategies.
*/
public final class RouteTable implements Inspectable {
/**
* The obvious implementation of this class is a mapping from GUID to
* ReplyHandler's. The problem with this representation is it's hard to
* implement removeReplyHandler efficiently. You either have to keep the
* references to the ReplyHandler (which wastes memory) or iterate through
* the entire table to clean all references (which wastes time AND removes
* valuable information for preventing duplicate queries).
*
* Instead we use a layer of indirection. _newMap/_oldMap maps GUIDs to
* integers, which act as IDs for each connection. _idMap maps IDs to
* ReplyHandlers. _handlerMap maps ReplyHandler to IDs. So to clean up a
* connection, we just purge the entries from _handlerMap and _idMap; there
* is no need to iterate through the entire GUID mapping. Adding GUIDs and
* routing replies are still constant-time operations.
*
* IDs are allocated sequentially according with the nextID variable. The
* field does "wrap around" after reaching the maximum integer value.
* Though no two open connections will have the same ID--we check
* _idMap--there is a very low probability that an ID in _map could be
* prematurely reused.
*
* To approximate FIFO behavior, we keep two sets around, _newMap and
* _oldMap. Every few seconds, when the system time is greater than
* nextSwitch, we clear _oldMap and replace it with _newMap.
* (DuplicateFilter uses the same trick.) In this way, we remember the last
* N to 2N minutes worth of GUIDs. This is superior to a fixed size route
* table.
*
* For flow-control reasons, we also store the number of bytes routed per
* GUID in each table. Hence the RouteTableEntry class.
*
* INVARIANT: keys of _newMap and _oldMap are disjoint
* INVARIANT: _idMap and _replyMap are inverses
*
* TODO3: if IDs were stored in each ReplyHandler, we would not need
* _replyMap. Better yet, if the values of _map were indices (with tags)
* into ConnectionManager._initialized[Client]Connections, we would not
* need _idMap either. However, this increases dependencies.
*/
private Map<byte[], RouteTableEntry> _newMap = new ExperimentalGUIDMap();
private Map<byte[], RouteTableEntry> _oldMap= new ExperimentalGUIDMap();
private int _mseconds;
private long _nextSwitchTime;
private int _maxSize;
private Map<Integer, ReplyHandler> _idMap = new HashMap<Integer, ReplyHandler>();
private Map<ReplyHandler, Integer> _handlerMap = new HashMap<ReplyHandler, Integer>();
private int _nextID;
/** Values stored in _newMap/_oldMap. */
private static final class RouteTableEntry implements ResultCounter {
/** The numericID of the reply connection. */
private int handlerID;
/** The bytes already routed for this GUID. */
private int bytesRouted;
/** The number of replies already routed for this GUID. */
private int repliesRouted;
/** The number of replies for partial files already routed for this GUID */
private int partialRepliesRouted;
/** The number of replies not counted for flow control */
private int repliesNotCounted;
/** The ttl associated with this RTE - meaningful only if > 0. */
private byte ttl = 0;
/** The class C networks that have returned a reply for this query */
private final ClassCNetworks classCnetworks = new ClassCNetworks();
/** Timestamp when this entry was created */
private final long creationTime = System.currentTimeMillis();
/** The times when results for this query arrived */
private final List<Double> resultTimeStamps = new ArrayList<Double>();
/** The number of results that came each time */
private final List<Double> resultCounts = new ArrayList<Double>();
/** The network from which the replies came */
private final int[] networks = new int[4];
/** The hops of the replies */
private final int[] hops = new int[5];
/** The ttls of the replies */
private final int[] ttls = new int[5];
/** Creates a new entry for the given ID, with zero bytes routed. */
RouteTableEntry(int handlerID) {
this.handlerID = handlerID;
this.bytesRouted = 0;
this.repliesRouted = 0;
this.repliesNotCounted = 0;
}
public void setTTL(byte ttl) { this.ttl = ttl; }
public byte getTTL() { return ttl; }
/** Accessor for the number of results for this entry. */
public int getNumResults() {
return Math.max(0, repliesRouted - partialRepliesRouted);
}
void updateClassCNetworks(int classCNetwork, int numReplies) {
classCnetworks.add(classCNetwork, numReplies);
}
void timeStampResults(int count) {
resultTimeStamps.add((double)(System.currentTimeMillis() - creationTime));
resultCounts.add((double)count);
}
void countHopsTTLNet(Network network, byte hop, byte ttl) {
networks[Math.max(0,Math.min(network.ordinal(),networks.length - 1))]++;
hops[Math.min(hops.length - 1, Math.max(0,hop-1))]++;
ttls[Math.min(ttls.length - 1, Math.max(0,ttl-1))]++;
}
}
/**
* Creates a new route table with enough space to hold the last seconds to
* 2*seconds worth of entries, or maxSize elements, whichever is smaller
* [sic].
*
* Typically maxSize is very large, and serves only as a guarantee to
* prevent worst case behavior. Actually 2*maxSize elements can be held in
* this in the worst case.
*/
public RouteTable(int seconds, int maxSize) {
this._mseconds=seconds*1000;
this._nextSwitchTime=System.currentTimeMillis()+_mseconds;
this._maxSize=maxSize;
}
/**
* Adds a new routing entry.
*
* @requires guid and c are non-null, guid.length==16
* @modifies this
* @effects if replyHandler is open, adds the routing entry to this,
* replacing any routing entries for guid. This has effect of
* "renewing" guid. Otherwise returns without modifying this.
*
* @return the <tt>RouteTableEntry</tt> entered into the routing
* tables, or <tt>null</tt> if it could not be entered
*/
public synchronized ResultCounter routeReply(byte[] guid,
ReplyHandler replyHandler) {
repOk();
purge();
if(replyHandler == null) {
throw new NullPointerException("null reply handler");
}
if (! replyHandler.isOpen())
return null;
//First clear out any old entries for the guid, memorizing the volume
//routed if found. Note that if the guid is found in _newMap, we don't
//need to look in _oldMap.
int id=handler2id(replyHandler).intValue();
RouteTableEntry entry = _newMap.remove(guid);
if (entry==null)
entry = _oldMap.remove(guid);
//Now map the guid to the new reply handler, using the volume routed if
//found, or zero otherwise.
if (entry==null)
entry=new RouteTableEntry(id);
else
entry.handlerID=id; //avoids allocation
_newMap.put(guid, entry);
return entry;
}
/**
* Adds a new routing entry if one doesn't exist.
*
* @requires guid and c are non-null, guid.length==16
* @modifies this
* @effects if no routing table entry for guid exists in this
* (including null mappings from calls to removeReplyHandler) and
* replyHandler is still open, adds the routing entry to this
* and returns true. Otherwise returns false, without modifying this.
*/
public synchronized ResultCounter tryToRouteReply(byte[] guid,
ReplyHandler replyHandler) {
repOk();
purge();
assert replyHandler != null;
assert guid!=null : "Null GUID in tryToRouteReply";
if (! replyHandler.isOpen())
return null;
if(!_newMap.containsKey(guid) && !_oldMap.containsKey(guid)) {
int id=handler2id(replyHandler).intValue();
RouteTableEntry entry = new RouteTableEntry(id);
_newMap.put(guid, entry);
//_newMap.put(guid, new RouteTableEntry(id));
return entry;
} else {
return null;
}
}
/** Optional operation - if you want to remember the TTL associated with a
* counter, in order to allow for extendable execution, you can set the TTL
* a message (guid).
* @param ttl should be greater than 0.
* @exception IllegalArgumentException thrown if !(ttl > 0), or if entry is
* null or is not something I recognize. So only put in what I dole out.
*/
public synchronized void setTTL(ResultCounter entry, byte ttl) {
if (entry == null)
throw new IllegalArgumentException("Null entry!!");
if (!(entry instanceof RouteTableEntry))
throw new IllegalArgumentException("entry is not recognized.");
if (!(ttl > 0))
throw new IllegalArgumentException("Input TTL too small: " + ttl);
((RouteTableEntry)entry).setTTL(ttl);
}
/** Synchronizes a TTL get test with a set test.
* @param getTTL the ttl you want getTTL() to be in order to setTTL().
* @param setTTL the ttl you want to setTTL() if getTTL() was correct.
* @return true if the TTL was set as you desired.
* @throws IllegalArgumentException if getTTL or setTTL is less than 1, or
* if setTTL < getTTL
*/
public synchronized boolean getAndSetTTL(byte[] guid, byte getTTL,
byte setTTL) {
if ((getTTL < 1) || (setTTL <= getTTL))
throw new IllegalArgumentException("Bad ttl input (get/set): " +
getTTL + "/" + setTTL);
RouteTableEntry entry = _newMap.get(guid);
if (entry==null)
entry = _oldMap.get(guid);
if ((entry != null) && (entry.getTTL() == getTTL)) {
entry.setTTL(setTTL);
return true;
}
return false;
}
/**
* Looks up the reply route for a given guid.
*
* @requires guid.length==16
* @effects returns the corresponding ReplyHandler for this GUID.
* Returns null if no mapping for guid, or guid maps to null (i.e.,
* to a removed ReplyHandler.
*/
public synchronized ReplyHandler getReplyHandler(byte[] guid) {
//no purge
repOk();
//Look up guid in _newMap. If not there, check _oldMap.
RouteTableEntry entry = _newMap.get(guid);
if (entry==null)
entry = _oldMap.get(guid);
//Note that id2handler may return null.
return (entry==null) ? null : id2handler(new Integer(entry.handlerID));
}
public synchronized ReplyRoutePair getReplyHandler(byte[] guid,
int replyBytes, short numReplies, short partialReplies) {
return getReplyHandler(guid, replyBytes, numReplies, partialReplies, 0, true);
}
/**
* Looks up the reply route and route volume for a given guid, incrementing
* the count of bytes routed for that GUID.
*
* @param classCNetwork integer representing the classC network the replies
* came from. 0 if it should not be counted (as 0 is not a valid classC network).
* @param classCNetwork the class c network the reply came from, 0 if the class c
* networ should not be counted
* @requires guid.length==16
* @effects if no mapping for guid, or guid maps to null (i.e., to a removed
* ReplyHandler) returns null. Otherwise returns a tuple containing the
* corresponding ReplyHandler for this GUID along with the volume of
* messages already routed for that guid. Afterwards, increments the reply
* count by replyBytes.
*/
public synchronized ReplyRoutePair getReplyHandler(byte[] guid,
int replyBytes,
short numReplies,
short partialReplies,
int classCNetwork,
boolean count) {
//no purge
repOk();
//Look up guid in _newMap. If not there, check _oldMap.
RouteTableEntry entry = _newMap.get(guid);
if (entry==null)
entry = _oldMap.get(guid);
//If no mapping for guid, or guid maps to a removed reply handler,
//return null.
if (entry==null)
return null;
ReplyHandler handler=id2handler(new Integer(entry.handlerID));
if (handler==null)
return null;
//Increment count, returning old count in tuple.
ReplyRoutePair ret =
new ReplyRoutePair(handler, entry.bytesRouted, entry.repliesRouted);
if(count) {
entry.bytesRouted += replyBytes;
entry.repliesRouted += numReplies;
entry.partialRepliesRouted += partialReplies;
} else {
entry.repliesNotCounted += numReplies;
}
if (classCNetwork != 0)
entry.updateClassCNetworks(classCNetwork, numReplies);
return ret;
}
/** Remembers that the specified number of results came now */
public synchronized void timeStampResults(QueryReply reply) {
RouteTableEntry entry = _newMap.get(reply.getGUID());
if (entry==null)
entry = _oldMap.get(reply.getGUID());
if (entry==null)
return;
entry.timeStampResults(reply.getUniqueResultCount());
}
public synchronized void countHopsTTLNet(QueryReply reply) {
RouteTableEntry entry = _newMap.get(reply.getGUID());
if (entry==null)
entry = _oldMap.get(reply.getGUID());
if (entry==null)
return;
entry.countHopsTTLNet(reply.getNetwork(), reply.getHops(), reply.getTTL());
}
/** The return value from getReplyHandler. */
public static final class ReplyRoutePair {
private final ReplyHandler handler;
private final int volume;
private final int REPLIES_ROUTED;
ReplyRoutePair(ReplyHandler handler, int volume, int hits) {
this.handler = handler;
this.volume = volume;
REPLIES_ROUTED = hits;
}
/** Returns the ReplyHandler to route your message */
public ReplyHandler getReplyHandler() { return handler; }
/** Returns the volume of messages already routed for the given GUID. */
public int getBytesRouted() { return volume; }
/**
* Accessor for the number of query results that have been routed
* for the GUID that identifies this <tt>ReplyRoutePair</tt>.
*
* @return the number of query results that have been routed for this
* guid
*/
public int getResultsRouted() { return REPLIES_ROUTED; }
}
/**
* Clears references to a given ReplyHandler.
*
* @modifies this
* @effects replaces all entries [guid, rh2] s.t.
* rh2.equals(replyHandler) with entries [guid, null]. This operation
* runs in constant time. [sic]
*/
public synchronized void removeReplyHandler(ReplyHandler replyHandler) {
//no purge
repOk();
//The aggressive asserts below are to make sure bug X75 has been fixed.
assert replyHandler!=null : "Null replyHandler in removeReplyHandler";
//Note that _map is not modified. See overview of class for rationale.
//Also, handler2id may actually allocate a new ID for replyHandler, when
//killing a connection for which we've routed no replies. That's ok;
//we'll just clean up the new ID immediately.
Integer id=handler2id(replyHandler);
_idMap.remove(id);
_handlerMap.remove(replyHandler);
}
/**
* @modifies nextID, _handlerMap, _idMap
* @effects returns a unique ID for the given handler, updating
* _handlerMap and _idMap if handler has not been encountered before.
* With very low probability, the returned id may be a value _map.
*/
private Integer handler2id(ReplyHandler handler) {
//Have we encountered this handler recently? If so, return the id.
Integer id = _handlerMap.get(handler);
if (id!=null)
return id;
//Otherwise return the next free id, searching in extremely rare cases
//if needed. Note that his enters an infinite loop if all 2^32 IDs are
//taken up. BFD.
while (true) {
//don't worry about overflow; Java wraps around TODO1?
id=new Integer(_nextID++);
if (_idMap.get(id)==null)
break;
}
_handlerMap.put(handler, id);
_idMap.put(id, handler);
return id;
}
/**
* Returns the ReplyHandler associated with the following ID, or
* null if none.
*/
private ReplyHandler id2handler(Integer id) {
return _idMap.get(id);
}
/**
* Purges old entries.
*
* @modifies _nextSwitchTime, _newMap, _oldMap
* @effects if the system time is less than _nextSwitchTime, returns
* false. Otherwise, clears _oldMap and swaps _oldMap and _newMap,
* updates _nextSwitchTime, and returns true.
*/
private final boolean purge() {
long now=System.currentTimeMillis();
if (now<_nextSwitchTime && _newMap.size()<_maxSize)
//not enough time has elapsed and sets too small
return false;
//System.out.println(now+" "+this.hashCode()+" purging "
// +_oldMap.size()+" old, "
// +_newMap.size()+" new");
_oldMap.clear();
Map<byte[], RouteTableEntry> tmp=_oldMap;
_oldMap=_newMap;
_newMap=tmp;
_nextSwitchTime=now+_mseconds;
return true;
}
@Override
public synchronized String toString() {
//Inefficient, but this is only for debugging anyway.
StringBuilder buf=new StringBuilder("{");
Map<byte[], RouteTableEntry> bothMaps=new TreeMap<byte[], RouteTableEntry>(new GUID.GUIDByteComparator());
bothMaps.putAll(_oldMap);
bothMaps.putAll(_newMap);
Iterator<byte[]> iter=bothMaps.keySet().iterator();
while (iter.hasNext()) {
byte[] key = iter.next();
buf.append(new GUID(key)); // GUID
buf.append("->");
int id= bothMaps.get(key).handlerID;
ReplyHandler handler=id2handler(new Integer(id));
buf.append(handler==null ? "null" : handler.toString());//connection
if (iter.hasNext())
buf.append(", ");
}
buf.append("}");
return buf.toString();
}
// private static boolean warned=false;
/** Tests internal consistency. VERY slow. */
private final void repOk() {
/*
if (!warned) {
System.err.println(
"WARNING: RouteTable.repOk enabled. Expect performance problems!");
warned=true;
}
//Check that _idMap is inverse of _handlerMap...
for (Iterator iter=_idMap.keySet().iterator(); iter.hasNext(); ) {
Integer key=(Integer)iter.next();
ReplyHandler value=(ReplyHandler)_idMap.get(key);
Assert.that(_handlerMap.get(value)==key);
}
//..and vice versa
for (Iterator iter=_handlerMap.keySet().iterator(); iter.hasNext(); ) {
ReplyHandler key=(ReplyHandler)iter.next();
Integer value=(Integer)_handlerMap.get(key);
Assert.that(_idMap.get(value)==key);
}
//Check that keys of _newMap aren't in _oldMap, values are RouteTableEntry
for (Iterator iter=_newMap.keySet().iterator(); iter.hasNext(); ) {
byte[] guid=(byte[])iter.next();
Assert.that(! _oldMap.containsKey(guid));
Assert.that(_newMap.get(guid) instanceof RouteTableEntry);
}
//Check that keys of _oldMap aren't in _newMap
for (Iterator iter=_oldMap.keySet().iterator(); iter.hasNext(); ) {
byte[] guid=(byte[])iter.next();
Assert.that(! _newMap.containsKey(guid));
Assert.that(_oldMap.get(guid) instanceof RouteTableEntry);
}
*/
}
/**
* @param guid a guid of a message we wish to route
* @return the same guid if the zero guid experiment is enabled,
* otherwise a clone with the oob-affected bytes zeroed.
*/
private static final byte [] zeroOOBBytes(byte [] guid) {
if (!MessageSettings.GUID_ZERO_EXPERIMENT.getValue())
return guid;
guid = guid.clone();
for (int i : new int[]{0,1,2,3,13,14})
guid[i] = 0;
return guid;
}
/**
* A map that can optionally zero out the OOB-mutated bytes of a guid.
*/
private static class ExperimentalGUIDMap extends TreeMap<byte [], RouteTableEntry> {
ExperimentalGUIDMap() {
super(GUID.GUID_BYTE_COMPARATOR);
}
@Override
public boolean containsKey(Object key) {
if (key instanceof byte[])
key = zeroOOBBytes((byte[]) key);
return super.containsKey(key);
}
@Override
public RouteTableEntry get(Object key) {
if (key instanceof byte[])
key = zeroOOBBytes((byte[]) key);
return super.get(key);
}
@Override
public RouteTableEntry put(byte[] key, RouteTableEntry value) {
key = zeroOOBBytes(key);
return super.put(key, value);
}
@Override
public RouteTableEntry remove(Object key) {
if (key instanceof byte[])
key = zeroOOBBytes((byte[]) key);
return super.remove(key);
}
}
/**
* An actual dump of the routing table. May get big, so
* its a good idea to first inspect the stats to see how many
* entries there are.
*/
@Override
public synchronized Object inspect() {
Map<String, Object> ret = new HashMap<String, Object>();
Iterable<Map.Entry<byte[], RouteTableEntry>> bothMaps =
new MultiIterable<Map.Entry<byte[],RouteTableEntry>>(_newMap.entrySet(),_oldMap.entrySet());
for (Map.Entry<byte[], RouteTableEntry> entry : bothMaps) {
RouteTableEntry e = entry.getValue();
Map<String, Object> m = new HashMap<String, Object>();
m.put("br", e.bytesRouted);
m.put("ttl", e.ttl);
m.put("rr", e.repliesRouted);
m.put("rnc", e.repliesNotCounted);
m.put("prr", e.partialRepliesRouted);
m.put("cc", e.classCnetworks.getMap());
m.put("rt", e.resultTimeStamps);
m.put("rc", e.resultCounts);
m.put("ct", e.creationTime);
m.put("id", e.handlerID);
m.put("nets", getBytes(e.networks));
m.put("hops", getBytes(e.hops));
m.put("ttls", getBytes(e.ttls));
ret.put(Base32.encode(entry.getKey()), m);
}
for (int id : _idMap.keySet()) {
ReplyHandler r = _idMap.get(id);
Map<String,Object> m = new HashMap<String,Object>();
m.put("ip",r.getAddress());
m.put("port", r.getPort());
m.put("cguid",r.getClientGUID());
ret.put(String.valueOf(id),m);
}
return ret;
}
private byte [] getBytes(int []ints) {
ByteBuffer b = ByteBuffer.allocate(ints.length * 4);
b.asIntBuffer().put(ints);
return b.array();
}
}