package com.limegroup.gnutella.connection; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.messages.Message; /** * A queue of messages organized by type. Used by ManagedConnection to * implement the SACHRIFC flow control algorithm. Delegates to multiple * MessageQueues, making sure that no one type of message dominates traffic. */ public class CompositeQueue implements MessageQueue { /* * IMPLEMENTATION NOTE: this class uses the SACHRIFC algorithm described at * http://www.limewire.com/developer/sachrifc.txt. The basic idea is to use * one buffer for each message type. Messages are removed from the buffers in * a biased round-robin fashion. This prioritizes some messages types while * preventing any one message type from dominating traffic. Query replies * are further prioritized by "GUID volume", i.e., the number of bytes * already routed for that GUID. Other messages are sorted by time and * removed in a LIFO [sic] policy. This, coupled with timeouts, reduces * latency. */ ///////////////////////////////// Parameters ////////////////////////////// /** * The producer's queues, one priority per mesage type. * INVARIANT: _queues.length==PRIORITIES */ private MessageQueue[] _queues = new MessageQueue[PRIORITIES]; /** * The number of queued messages. Maintained for performance. * INVARIANT: _queued == sum of _queues[i].size() */ private int _queued = 0; /** * The current priority of the queue we're looking at. Necessary to preserve * over multiple iterations of removeNext to ensure the queue is extracted in * order, though not necessary to ensure all messages are correctly set. * As an optimization, if a message is the only one queued, _priority is set * to be that message's queued. */ private int _priority = 0; /** * The priority of the last message that was added. If removeNext detects * that it has gone through a cycle (and everything returned null), it marks * the next removeNext to use the priorityHint to jump-start on the last * added message. */ private int _priorityHint = 0; /** * The status of removeNext. True if the last call was a complete cycle * through all potential fields. */ private boolean _cycled = true; /** * The number of messages we've dropped while adding or retrieving messages. */ private int _dropped = 0; /** * A larger queue size than the standard to accomodate higher priority * messages, such as queries and query hits. */ private static final int BIG_QUEUE_SIZE = 100; /** * The standard queue size for smaller messages so that we don't waste too * much memory on lower priority messages. */ private static final int QUEUE_SIZE = 1; /** The max time to keep reply messages and pushes in the queues, in milliseconds */ private static final int BIG_QUEUE_TIME=10*1000; /** The max time to keep queries, pings, and pongs in the queues, in milliseconds */ public static final int QUEUE_TIME=5*1000; /** The number of different priority levels. */ private static final int PRIORITIES = 8; /** * Names for each priority. "Other" includes QRP messages and is NOT * reordered. These numbers do NOT translate directly to priorities; * that's determined by the cycle fields passed to MessageQueue. */ private static final int PRIORITY_WATCHDOG=0; private static final int PRIORITY_PUSH=1; private static final int PRIORITY_QUERY_REPLY=2; private static final int PRIORITY_QUERY=3; //TODO: separate requeries private static final int PRIORITY_PING_REPLY=4; private static final int PRIORITY_PING=5; private static final int PRIORITY_OTHER=6; private static final int PRIORITY_OUR_QUERY=7; // seperate for re-originated leaf-queries. /** * Constructs a new queue with the default sizes. */ public CompositeQueue() { this(BIG_QUEUE_TIME, BIG_QUEUE_SIZE, QUEUE_TIME, QUEUE_SIZE); } /** * Constructs a new queue with the given message buffer sizes. */ public CompositeQueue(int largeTime, int largeSize, int normalTime, int normalSize) { _queues[PRIORITY_WATCHDOG] = new SimpleMessageQueue(1, Integer.MAX_VALUE, largeSize, true); // LIFO _queues[PRIORITY_PUSH] = new PriorityMessageQueue(6, largeTime, largeSize); _queues[PRIORITY_QUERY_REPLY] = new PriorityMessageQueue(6, largeTime, largeSize); _queues[PRIORITY_QUERY] = new PriorityMessageQueue(3, normalTime, largeSize); _queues[PRIORITY_PING_REPLY] = new PriorityMessageQueue(1, normalTime, normalSize); _queues[PRIORITY_PING] = new PriorityMessageQueue(1, normalTime, normalSize); _queues[PRIORITY_OUR_QUERY] = new PriorityMessageQueue(10, largeTime, largeSize); _queues[PRIORITY_OTHER] = new SimpleMessageQueue(1, Integer.MAX_VALUE, largeSize, false); // FIFO } /** * Adds m to this, possibly dropping some messages in the process; call * resetDropped to get the count of dropped messages. * @see resetDropped */ public void add(Message m) { //Add m to appropriate buffer int priority = calculatePriority(m); MessageQueue queue = _queues[priority]; queue.add(m); //Update statistics int dropped = queue.resetDropped(); _dropped += dropped; _queued += 1-dropped; // Remember the priority so we can set it if we detect we cycled. _priorityHint = priority; } /** * Returns the send priority for the given message, with higher number for * higher priorities. TODO: this method will eventually be moved to * MessageRouter and account for number of reply bytes. */ private int calculatePriority(Message m) { byte opcode=m.getFunc(); switch (opcode) { case Message.F_QUERY: return ((QueryRequest)m).isOriginated() ? PRIORITY_OUR_QUERY : PRIORITY_QUERY; case Message.F_QUERY_REPLY: return PRIORITY_QUERY_REPLY; case Message.F_PING_REPLY: return (m.getHops()==0 && m.getTTL()<=2) ? PRIORITY_WATCHDOG : PRIORITY_PING_REPLY; case Message.F_PING: return (m.getHops()==0 && m.getTTL()==1) ? PRIORITY_WATCHDOG : PRIORITY_PING; case Message.F_PUSH: return PRIORITY_PUSH; default: return PRIORITY_OTHER; //includes QRP Tables } } /** * Removes and returns the next message to send from this. Returns null if * there are no more messages to send. The returned message is guaranteed * be younger than TIMEOUT milliseconds. Messages may be dropped in the * process; find out how many by calling resetDropped(). For this reason * note that size()>0 does not imply that removeNext()!=null. * @return the next message, or null if none * @see resetDropped */ public Message removeNext() { if(_cycled) { _cycled = false; _priority = _priorityHint; _queues[_priority].resetCycle(); } //Try all priorities in a round-robin fashion until we find a //non-empty buffer. This degenerates in performance if the queue //contains only a single type of message. while (_queued > 0) { MessageQueue queue = _queues[_priority]; //Try to get a message from the current queue. Message m = queue.removeNext(); int dropped = queue.resetDropped(); _dropped += dropped; _queued -= (m == null ? 0 : 1) + dropped; //maintain invariant if (m != null) return m; //No luck? Go on to next queue. _priority = (_priority + 1) % PRIORITIES; _queues[_priority].resetCycle(); } _cycled = true; //Nothing to send. return null; } /** * Returns the number of dropped messages since the last call to * resetDropped(). */ public final int resetDropped() { int ret = _dropped; _dropped = 0; return ret; } /** * Returns the number of messages in this. Note that size()>0 does not * imply that removeNext()!=null; messages may be expired upon sending. */ public int size() { return _queued; } /** Determines if this is empty. */ public boolean isEmpty() { return _queued == 0; } /** Does nothing. */ public void resetCycle() {} }