package com.limegroup.gnutella;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import com.limegroup.gnutella.search.ResultCounter;
/**
* 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 {
/**
* 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 dependenceies.
*/
private Map /* byte[] -> RouteTableEntry */ _newMap=
new TreeMap(new GUID.GUIDByteComparator());
private Map /* byte[] -> RouteTableEntry */ _oldMap=
new TreeMap(new GUID.GUIDByteComparator());
private int _mseconds;
private long _nextSwitchTime;
private int _maxSize;
private Map /* Integer -> ReplyHandler */ _idMap=new HashMap();
private Map /* ReplyHandler -> Integer */ _handlerMap=new HashMap();
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 ttl associated with this RTE - meaningful only if > 0. */
private byte ttl = 0;
/** Creates a new entry for the given ID, with zero bytes routed. */
RouteTableEntry(int handlerID) {
this.handlerID = handlerID;
this.bytesRouted = 0;
this.repliesRouted = 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 repliesRouted; }
}
/**
* 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=(RouteTableEntry)_newMap.remove(guid);
if (entry==null)
entry=(RouteTableEntry)_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.that(replyHandler != null);
Assert.that(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=(RouteTableEntry)_newMap.get(guid);
if (entry==null)
entry=(RouteTableEntry)_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=(RouteTableEntry)_newMap.get(guid);
if (entry==null)
entry=(RouteTableEntry)_oldMap.get(guid);
//Note that id2handler may return null.
return (entry==null) ? null : id2handler(new Integer(entry.handlerID));
}
/**
* Looks up the reply route and route volume for a given guid, incrementing
* the count of bytes routed for that GUID.
*
* @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) {
//no purge
repOk();
//Look up guid in _newMap. If not there, check _oldMap.
RouteTableEntry entry=(RouteTableEntry)_newMap.get(guid);
if (entry==null)
entry=(RouteTableEntry)_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);
entry.bytesRouted += replyBytes;
entry.repliesRouted += numReplies;
return ret;
}
/** 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.that(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=(Integer)_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 (ReplyHandler)_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 tmp=_oldMap;
_oldMap=_newMap;
_newMap=tmp;
_nextSwitchTime=now+_mseconds;
return true;
}
public synchronized String toString() {
//Inefficient, but this is only for debugging anyway.
StringBuffer buf=new StringBuffer("{");
Map bothMaps=new TreeMap(new GUID.GUIDByteComparator());
bothMaps.putAll(_oldMap);
bothMaps.putAll(_newMap);
Iterator iter=bothMaps.keySet().iterator();
while (iter.hasNext()) {
byte[] key=(byte[])iter.next();
buf.append(new GUID(key)); // GUID
buf.append("->");
int id=((RouteTableEntry)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);
}
*/
}
}