package freenet.node;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import freenet.io.comm.DMT;
import freenet.support.DoublyLinkedList;
import freenet.support.DoublyLinkedListImpl;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.MutableBoolean;
/**
* Queue of messages to send to a node. Ordered first by priority then by time.
* Will soon be round-robin between different transfers/UIDs/clients too.
* @author Matthew Toseland <toad@amphibian.dyndns.org> (0xE43DA450)
*/
public class PeerMessageQueue {
private static volatile boolean logMINOR;
private static volatile boolean logDEBUG;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
}
});
}
private final PrioQueue[] queuesByPriority;
private boolean mustSendLoadRT;
private boolean mustSendLoadBulk;
private class PrioQueue {
// FIXME refactor into PrioQueue and RoundRobinByUIDPrioQueue
PrioQueue(long timeout, boolean timeoutSinceLastSend) {
this.timeout = timeout;
this.roundRobinBetweenUIDs = timeoutSinceLastSend;
}
/** The timeout, period after which messages become urgent. */
final long timeout;
/** If true, do round-robin between UID's, and count the timeout relative
* to the last send. Block transfers need this - both realtime and bulk. */
final boolean roundRobinBetweenUIDs;
private class Items extends DoublyLinkedListImpl.Item<Items> {
/** List of messages to send. Stuff to send first is at the beginning. */
final LinkedList<MessageItem> items;
final long id;
long timeLastSent;
Items(long id, long initialTimeLastSent) {
items = new LinkedList<MessageItem>();
this.id = id;
timeLastSent = initialTimeLastSent;
}
public void addLast(MessageItem item) {
items.addLast(item);
}
public void addFirst(MessageItem item) {
items.addFirst(item);
}
public boolean remove(MessageItem item) {
return items.remove(item);
}
@Override
public String toString() {
return super.toString()+":"+id+":"+items.size()+":"+timeLastSent;
}
}
/** Maximum inter-packet time is 2 minutes for a block transfer (when we have bulk
* flag this will be no higher, and it might be reduced to 30 seconds). Requests
* can wait for 2 minutes now, maybe 10 minutes in future, but round-robin is
* intended for frequent messages - it doesn't matter in that case. So 3 minutes
* is plenty. */
static final long FORGET_AFTER = 3*60*1000;
/** Using DoublyLinkedListImpl so that we can move stuff around without the
* iterator failing, and also delete efficiently. Ordered by timeLastSent,
* NOT by timeout. Items we have not yet sent are at the beginning with
* timeLastSent = -1. */
DoublyLinkedListImpl<Items> nonEmptyItemsWithID;
/** Items which have been sent within the last 10 minutes, so we need to track
* them for good round-robin, but which we don't have anything queued on right now. */
DoublyLinkedListImpl<Items> emptyItemsWithID;
Map<Long, Items> itemsByID;
/** Non-urgent messages. Same order as in Items, so stuff to send first is at
* the beginning. */
LinkedList<MessageItem> itemsNonUrgent;
// Construct structures lazily, we're protected by the overall synchronized.
/** Add a new message. For a normal priority level, we just add it to the end of the list.
* It will be sent after the messages that are already queued, and its deadline is effectively
* the time it was submitted plus the timeout. For a priority level using round robin between
* peers, it is the same unless we have recently sent a message with the same UID. If we have,
* the timeout is relative to the last send. */
public void addLast(MessageItem item) {
// Clear the deadline for the item.
item.clearDeadline();
if(logMINOR) checkOrder();
if(roundRobinBetweenUIDs) {
long id = item.getID();
if(itemsByID != null) {
Items it = itemsByID.get(id);
if(it != null && it.timeLastSent > 0 && it.timeLastSent + timeout <= System.currentTimeMillis()) {
it.addLast(item);
if(it.getParent() == emptyItemsWithID)
moveFromEmptyToNonEmptyBackward(it);
else
assert(it.getParent() == nonEmptyItemsWithID);
if(logMINOR) checkOrder();
return;
}
}
}
addToNonUrgent(item);
}
private void addToNonUrgent(MessageItem item) {
if(itemsNonUrgent == null)
itemsNonUrgent = new LinkedList<MessageItem>();
ListIterator<MessageItem> it = itemsNonUrgent.listIterator(itemsNonUrgent.size());
// MessageItem's can be created out of order, so the timestamps may not be consistent.
// CONCURRENCY: This is not a problem in addNonUrgentMessages() because it is always called from one thread.
while(true) {
if(!it.hasPrevious()) {
it.add(item);
if(logMINOR) checkOrder();
return;
}
MessageItem prev = it.previous();
if(item.submitted >= prev.submitted) {
it.next();
it.add(item);
if(logMINOR) checkOrder();
return;
}
}
}
private void moveToUrgent(long now) {
if(logMINOR) checkOrder();
if(itemsNonUrgent == null) return;
ListIterator<MessageItem> it = itemsNonUrgent.listIterator();
int moved = 0;
while(it.hasNext()) {
MessageItem item = it.next();
Items list = null;
long id = item.getID();
if(itemsByID != null)
list = itemsByID.get(id);
boolean moveIt = false;
if(list != null && roundRobinBetweenUIDs) {
if(list.timeLastSent + timeout <= now)
moveIt = true;
}
if(item.submitted + timeout <= now) {
moveIt = true;
}
if(moveIt) {
if(logMINOR) Logger.minor(this, "Moving message to urgent list: "+item);
if(logMINOR) checkOrder();
// Move to urgent list
if(itemsByID == null) {
itemsByID = new HashMap<Long, Items>();
if(nonEmptyItemsWithID == null)
nonEmptyItemsWithID = new DoublyLinkedListImpl<Items>();
list = new Items(id, item.submitted);
addToNonEmptyForward(list);
itemsByID.put(id, list);
if(logMINOR) checkOrder();
} else {
if(list == null) {
list = new Items(id, item.submitted);
if(nonEmptyItemsWithID == null)
nonEmptyItemsWithID = new DoublyLinkedListImpl<Items>();
addToNonEmptyForward(list);
itemsByID.put(id, list);
if(logMINOR) checkOrder();
} else {
if(list.items.isEmpty()) {
if(list.getParent() == nonEmptyItemsWithID) {
Logger.error(this, "Was empty but was in nonEmptyItemsWithID: "+list);
} else {
assert(list.getParent() == emptyItemsWithID);
// It already exists, so it has a valid time.
// Which is probably in the past, so use Forward.
// Must add it to the list before moving to non-empty because of assertion.
moveFromEmptyToNonEmptyForward(list);
}
} else {
assert(list.getParent() == nonEmptyItemsWithID);
}
if(logMINOR) checkOrder();
}
}
list.addLast(item);
it.remove();
moved++;
if(logMINOR) checkOrder();
} else if(!roundRobinBetweenUIDs)
break;
}
if(logDEBUG && moved > 0)
Logger.debug(this, "Moved "+moved+" items to urgent round-robin");
if(logMINOR) checkOrder();
}
private void moveFromEmptyToNonEmptyForward(Items list) {
// Presumably is in emptyItemsWithID
assert(list.items.isEmpty());
if(logMINOR) {
if(list.getParent() == nonEmptyItemsWithID) {
Logger.error(this, "Already in non-empty yet empty?!");
return;
}
}
if(emptyItemsWithID != null)
emptyItemsWithID.remove(list);
addToNonEmptyForward(list);
}
private void addToNonEmptyForward(Items list) {
if(nonEmptyItemsWithID == null)
nonEmptyItemsWithID = new DoublyLinkedListImpl<Items>();
Enumeration<Items> it = nonEmptyItemsWithID.elements();
while(it.hasMoreElements()) {
Items compare = it.nextElement();
if(compare.timeLastSent >= list.timeLastSent) {
nonEmptyItemsWithID.insertPrev(compare, list);
return;
}
}
nonEmptyItemsWithID.push(list);
}
private void moveFromEmptyToNonEmptyBackward(Items list) {
// Presumably is in emptyItemsWithID
emptyItemsWithID.remove(list);
addToNonEmptyBackward(list);
}
private void addToNonEmptyBackward(Items list) {
if(nonEmptyItemsWithID == null)
nonEmptyItemsWithID = new DoublyLinkedListImpl<Items>();
Enumeration<Items> it = nonEmptyItemsWithID.reverseElements();
while(it.hasMoreElements()) {
Items compare = it.nextElement();
if(compare.timeLastSent <= list.timeLastSent) {
nonEmptyItemsWithID.insertNext(compare, list);
return;
}
}
nonEmptyItemsWithID.unshift(list);
}
private void addToEmptyBackward(Items list) {
if(emptyItemsWithID == null)
emptyItemsWithID = new DoublyLinkedListImpl<Items>();
Enumeration<Items> it = emptyItemsWithID.reverseElements();
while(it.hasMoreElements()) {
Items compare = it.nextElement();
if(compare.timeLastSent <= list.timeLastSent) {
emptyItemsWithID.insertNext(compare, list);
return;
}
}
emptyItemsWithID.unshift(list);
}
/** Add a new message to the beginning i.e. send it as soon as possible (e.g. if
* we tried to send it and failed); it is assumed to already be urgent. */
public void addFirst(MessageItem item) {
// Keep the old deadline for the item.
if(!roundRobinBetweenUIDs) {
addToNonUrgent(item);
return;
}
if(logMINOR) checkOrder();
long id = item.getID();
Items list;
if(itemsByID == null) {
itemsByID = new HashMap<Long, Items>();
if(nonEmptyItemsWithID == null)
nonEmptyItemsWithID = new DoublyLinkedListImpl<Items>();
list = new Items(id, -1);
addToNonEmptyForward(list);
itemsByID.put(id, list);
} else {
list = itemsByID.get(id);
if(list == null) {
list = new Items(id, -1);
if(nonEmptyItemsWithID == null)
nonEmptyItemsWithID = new DoublyLinkedListImpl<Items>();
nonEmptyItemsWithID.unshift(list);
itemsByID.put(id, list);
} else {
if(list.items.isEmpty()) {
assert(list.getParent() == emptyItemsWithID);
// It already exists, so it has a valid time.
// Which is probably in the past, so use Forward.
moveFromEmptyToNonEmptyForward(list);
} else
assert(list.getParent() == nonEmptyItemsWithID);
}
}
list.addFirst(item);
if(logMINOR) checkOrder();
}
public int size() {
int size = 0;
if(nonEmptyItemsWithID != null)
for(Items items : nonEmptyItemsWithID)
size += items.items.size();
if(itemsNonUrgent != null)
size += itemsNonUrgent.size();
return size;
}
public int addTo(MessageItem[] output, int ptr) {
if(nonEmptyItemsWithID != null)
for(Items list : nonEmptyItemsWithID)
for(MessageItem item : list.items)
output[ptr++] = item;
if(itemsNonUrgent != null)
for(MessageItem item : itemsNonUrgent)
output[ptr++] = item;
return ptr;
}
/** Check that nonEmptyItemsWithID is ordered correctly.
* LOCKING: Caller must synchronize on PeerMessageQueue.this. */
private void checkOrder() {
if(nonEmptyItemsWithID != null) {
long prev = -1;
Items prevItems = null;
for(Items items : nonEmptyItemsWithID) {
long thisTime = items.timeLastSent;
if(thisTime < prev)
Logger.error(this, "Inconsistent order in non empty items with ID: prev timeout was "+prev+" for "+prevItems+" but this timeout is "+thisTime+" for "+items, new Exception("error"));
prev = thisTime;
prevItems = items;
}
}
if(itemsNonUrgent != null) {
long prev = -1;
MessageItem prevItem = null;
for(MessageItem item : itemsNonUrgent) {
if(item.submitted < prev)
Logger.error(this, "Inconsistent order in itemsNonUrgent: prev submitted at "+prev+" but this at "+item.submitted+" prev is "+prevItem+" this is "+item);
prev = item.submitted;
prevItem = item;
}
}
}
/** Note that this does NOT consider the length of the queue, which can trigger a
* send. This is intentional, and is relied upon by the bulk-or-realtime logic in
* addMessages().
* @param t The initial urgent time. What we return must be less than or
* equal to this. Convenient for chaining.
* @param stopIfBeforeTime If the next urgent time is <= to this time,
* return immediately.
*/
public long getNextUrgentTime(long t, long stopIfBeforeTime) {
if(!roundRobinBetweenUIDs) {
if(itemsNonUrgent != null && !itemsNonUrgent.isEmpty()) {
t = Math.min(t, itemsNonUrgent.getFirst().submitted + timeout);
if(t <= stopIfBeforeTime) return t;
}
assert(nonEmptyItemsWithID == null);
assert(itemsByID == null);
} else {
if(nonEmptyItemsWithID != null) {
for(Items items : nonEmptyItemsWithID) {
if(items.items.size() == 0) continue;
if(items.timeLastSent > 0) {
t = Math.min(t, items.timeLastSent + timeout);
if(t <= stopIfBeforeTime) return t;
} else {
// It is possible that something requeued isn't urgent, so check anyway.
t = Math.min(t, items.items.getFirst().submitted + timeout);
if(t <= stopIfBeforeTime) return t;
}
}
}
if(itemsNonUrgent != null && !itemsNonUrgent.isEmpty()) {
for(MessageItem item : itemsNonUrgent) {
long uid = item.getID();
Items items = itemsByID == null ? null : itemsByID.get(uid);
if(items != null && items.timeLastSent > 0) {
t = Math.min(t, items.timeLastSent + timeout);
if(t <= stopIfBeforeTime) return t;
} else {
t = Math.min(t, item.submitted + timeout);
if(t <= stopIfBeforeTime) return t;
if(itemsByID == null) break; // Only the first one matters, since none have been sent.
}
}
}
}
return t;
}
/**
* Add the size of messages in this queue to <code>length</code> until
* length is larger than <code>maxSize</code>, or all messages have
* been added.
* @param length the starting length
* @param maxSize the size at which to stop
* @return the resulting length after adding messages
*/
public int addSize(int length, int maxSize) {
if(itemsNonUrgent != null) {
for(MessageItem item : itemsNonUrgent) {
int thisLen = item.getLength();
length += thisLen;
if(length > maxSize) return length;
}
}
if(nonEmptyItemsWithID != null) {
for(Items list : nonEmptyItemsWithID) {
for(MessageItem item : list.items) {
int thisLen = item.getLength();
length += thisLen;
if(length > maxSize) return length;
}
}
}
return length;
}
private MessageItem addNonUrgentMessages(long now, MutableBoolean addPeerLoadStatsRT, MutableBoolean addPeerLoadStatsBulk) {
if(logMINOR) checkOrder();
if(itemsNonUrgent == null) return null;
MessageItem ret;
for(ListIterator<MessageItem> items = itemsNonUrgent.listIterator();items.hasNext();) {
MessageItem item = items.next();
items.remove();
item.setDeadline(item.submitted + timeout);
ret = item;
if(itemsByID != null) {
long id = item.getID();
Items tracker = itemsByID.get(id);
if(tracker != null) {
tracker.timeLastSent = now;
DoublyLinkedList<? super Items> parent = tracker.getParent();
// Demote the corresponding tracker to maintain round-robin.
if(tracker.items.isEmpty()) {
if(logDEBUG) Logger.debug(this, "Moving "+tracker+" to end of empty list in addNonUrgentMessages");
if(emptyItemsWithID == null)
emptyItemsWithID = new DoublyLinkedListImpl<Items>();
if(parent == null) {
Logger.error(this, "Tracker is in itemsByID but not in either list! (empty)");
} else if(parent == emptyItemsWithID) {
// Normal. Remove it so we can re-add it in the right place.
emptyItemsWithID.remove(tracker);
} else if(parent == nonEmptyItemsWithID) {
Logger.error(this, "Tracker is in non empty items list when is empty");
nonEmptyItemsWithID.remove(tracker);
} else
assert(false);
addToEmptyBackward(tracker);
} else {
if(logDEBUG) Logger.debug(this, "Moving "+tracker+" to end of non-empty list in addNonUrgentMessages");
if(nonEmptyItemsWithID == null)
nonEmptyItemsWithID = new DoublyLinkedListImpl<Items>();
if(parent == null) {
Logger.error(this, "Tracker is in itemsByID but not in either list! (non-empty)");
} else if(parent == nonEmptyItemsWithID) {
// Normal. Remove it so we can re-add it in the right place.
nonEmptyItemsWithID.remove(tracker);
} else if(parent == emptyItemsWithID) {
Logger.error(this, "Tracker is in empty items list when is non-empty");
emptyItemsWithID.remove(tracker);
} else
assert(false);
addToNonEmptyBackward(tracker);
}
}
}
if(mustSendLoadRT && item.sendLoadRT && !addPeerLoadStatsRT.value) {
addPeerLoadStatsRT.value = true;
mustSendLoadRT = false;
} else if(mustSendLoadBulk && item.sendLoadBulk && !addPeerLoadStatsBulk.value) {
addPeerLoadStatsBulk.value = true;
mustSendLoadBulk = false;
}
if(logMINOR) checkOrder();
if(ret != null) return ret;
}
if(logMINOR) checkOrder();
return null;
}
/**
* Add messages to <code>messages</code> until there are no more
* messages to add.
*
* @param now the current time
* @param messages the list that messages will be added to
* @param maxMessages
* @return the size of <code>messages</code>, multiplied by -1 if there were
* messages that didn't fit
*/
private MessageItem addUrgentMessages(long now, MutableBoolean addPeerLoadStatsRT, MutableBoolean addPeerLoadStatsBulk) {
if(logMINOR) checkOrder();
MessageItem ret;
while(true) {
int lists = 0;
if(nonEmptyItemsWithID == null) {
if(logMINOR) Logger.minor(this, "No non-empty items to send, not sending any urgent messages");
return null;
}
lists += nonEmptyItemsWithID.size();
Items list = nonEmptyItemsWithID.head();
for(int i=0;i<lists && list != null;i++) {
if(logMINOR) checkOrder();
if(list.items.isEmpty()) {
// Should not happen, but check for it anyway since it keeps happening. :(
Logger.error(this, "List is in nonEmptyItemsWithID yet it is empty?!: "+list);
nonEmptyItemsWithID.remove(list);
addToEmptyBackward(list);
if(nonEmptyItemsWithID.isEmpty()) {
if(logMINOR) Logger.minor(this, "Run out of non-empty items to send");
return null;
}
list = nonEmptyItemsWithID.head();
continue;
}
MessageItem item = list.items.getFirst();
list.items.removeFirst();
// Move to end of list.
Items prev = list.getPrev();
nonEmptyItemsWithID.remove(list);
item.setDeadline(list.timeLastSent + timeout);
list.timeLastSent = now;
if(!list.items.isEmpty()) {
if(logDEBUG) Logger.debug(this, "Moving "+list+" to end of non empty list in addUrgentMessages");
addToNonEmptyBackward(list);
} else {
if(logDEBUG) Logger.debug(this, "Moving "+list+" to end of empty list in addUrgentMessages");
addToEmptyBackward(list);
}
if(prev == null)
list = nonEmptyItemsWithID.head();
else
list = prev.getNext();
ret = item;
if(mustSendLoadRT && item.sendLoadRT && !addPeerLoadStatsRT.value) {
addPeerLoadStatsRT.value = true;
mustSendLoadRT = false;
} else if(mustSendLoadBulk && item.sendLoadBulk && !addPeerLoadStatsBulk.value) {
addPeerLoadStatsBulk.value = true;
mustSendLoadBulk = false;
}
if(logMINOR) checkOrder();
if(ret != null) return ret;
}
if(logDEBUG)
Logger.debug(this, "No more messages queued at this priority");
if(logMINOR) checkOrder();
return null;
}
}
/**
* Add urgent messages, then non-urgent messages. Add a load message if need to.
* @param size
* @param now
* @param messages
* @param addPeerLoadStatsRT Will be set if the caller needs to include a load stats message for
* realtime (i.e. a realtime request completes etc).
* @param addPeerLoadStatsBulk Will be set if the caller needs to include a load stats message for
* bulk (i.e. a bulk request completes etc).
* @param incomplete Will be set if there were more messages but they did not fit. If this is
* not set, we can try another priority.
* @return
*/
MessageItem addPriorityMessages(long now, MutableBoolean addPeerLoadStatsRT, MutableBoolean addPeerLoadStatsBulk) {
// Urgent messages first.
if(logMINOR) {
int nonEmpty = nonEmptyItemsWithID == null ? 0 : nonEmptyItemsWithID.size();
int empty = emptyItemsWithID == null ? 0 : emptyItemsWithID.size();
int byID = itemsByID == null ? 0 : itemsByID.size();
if(nonEmpty + empty < byID) {
Logger.error(this, "Leaking itemsByID? non empty = "+nonEmpty+" empty = "+empty+" by ID = "+byID+" on "+this);
} else if(logDEBUG)
Logger.debug(this, "Items: non empty "+nonEmpty+" empty "+empty+" by ID "+byID+" on "+this);
}
if(roundRobinBetweenUIDs)
moveToUrgent(now);
clearOldNonUrgent(now);
if(roundRobinBetweenUIDs) {
MessageItem item = addUrgentMessages(now, addPeerLoadStatsRT, addPeerLoadStatsBulk);
if(item != null) return item;
} else {
assert(itemsByID == null);
}
// If no more urgent messages, try to add some non-urgent messages too.
return addNonUrgentMessages(now, addPeerLoadStatsRT, addPeerLoadStatsBulk);
}
private void clearOldNonUrgent(long now) {
if(logMINOR) checkOrder();
int removed = 0;
if(emptyItemsWithID == null) return;
while(true) {
if(logMINOR) checkOrder();
if(emptyItemsWithID.isEmpty()) return;
Items list = emptyItemsWithID.head();
if(!list.items.isEmpty()) {
// FIXME remove paranoia
Logger.error(this, "List with items in emptyItemsWithID!!");
emptyItemsWithID.remove(list);
addToNonEmptyBackward(list);
return;
}
if(list.timeLastSent == -1 || now - list.timeLastSent > FORGET_AFTER) {
// FIXME: Urgh, what a braindead API! remove(Object) on a Map<Long, Items> !?!?!?!
// Anyway we'd better check the return value!
Items old = itemsByID.remove(list.id);
if(old == null)
Logger.error(this, "List was not in the items by ID tracker: "+list.id);
else if(old != list)
Logger.error(this, "Different list in the items by ID tracker: "+old+" not "+list+" for "+list.id);
emptyItemsWithID.remove(list);
removed++;
} else {
if(logDEBUG && removed > 0)
Logger.debug(this, "Removed "+removed+" old empty UID trackers");
break;
}
}
}
public void clear() {
emptyItemsWithID = null;
nonEmptyItemsWithID = null;
itemsByID = null;
itemsNonUrgent = null;
if(logMINOR) checkOrder();
}
public boolean removeMessage(MessageItem item) {
if(logMINOR) checkOrder();
long id = item.getID();
Items list;
if(itemsByID != null) {
list = itemsByID.get(id);
if(list != null) {
if(list.remove(item)) {
if(list.items.isEmpty()) {
nonEmptyItemsWithID.remove(list);
addToEmptyBackward(list);
}
if(logMINOR) checkOrder();
return true;
}
}
}
if(logMINOR) checkOrder();
if(itemsNonUrgent != null)
return itemsNonUrgent.remove(item);
else
return false;
}
public void removeUIDs(Long[] list) {
if(logMINOR) checkOrder();
if(itemsByID == null) return;
for(Long l : list) {
Items items = itemsByID.get(l);
if(items == null) continue;
if(items.items.isEmpty()) {
itemsByID.remove(l);
assert(emptyItemsWithID != null);
assert(items.getParent() == emptyItemsWithID);
emptyItemsWithID.remove(items);
}
}
if(logMINOR) checkOrder();
}
public boolean isEmpty() {
if(itemsNonUrgent != null && !itemsNonUrgent.isEmpty()) {
return false;
}
if(nonEmptyItemsWithID != null) {
for(Items items : nonEmptyItemsWithID) {
if(items.items.size() == 0) continue;
return false;
}
}
return true;
}
}
PeerMessageQueue() {
queuesByPriority = new PrioQueue[DMT.NUM_PRIORITIES];
for(int i=0;i<queuesByPriority.length;i++) {
if(i == DMT.PRIORITY_BULK_DATA)
// Bulk: round-robin between UID's (timeout since last sent), long timeout.
queuesByPriority[i] = new PrioQueue(PacketSender.MAX_COALESCING_DELAY_BULK, true);
else if(i == DMT.PRIORITY_REALTIME_DATA)
// Realtime: round-robin between UID's (timeout since last sent), short timeout.
queuesByPriority[i] = new PrioQueue(PacketSender.MAX_COALESCING_DELAY, true);
else
// Everything else: Still round-robin between UID's, but timeout on submitted.
queuesByPriority[i] = new PrioQueue(PacketSender.MAX_COALESCING_DELAY, false);
}
}
/**
* Queue a <code>MessageItem</code> and return an estimate of the size of
* this queue. The value returned is the estimated number of bytes
* needed for sending the all messages in this queue. Note that if the
* returned estimate is higher than 1024, it might not cover all messages.
* @param item the <code>MessageItem</code> to queue
* @return an estimate of the size of this queue
*/
public synchronized int queueAndEstimateSize(MessageItem item, int maxSize) {
enqueuePrioritizedMessageItem(item);
int x = 0;
for(PrioQueue pq : queuesByPriority) {
if(pq.itemsNonUrgent != null) {
for(MessageItem it : pq.itemsNonUrgent) {
x += it.getLength() + 2;
if(x > maxSize)
break;
}
}
if(pq.nonEmptyItemsWithID != null) {
for(PrioQueue.Items q : pq.nonEmptyItemsWithID)
for(MessageItem it : q.items) {
x += it.getLength() + 2;
if(x > maxSize)
break;
}
}
}
return x;
}
public synchronized long getMessageQueueLengthBytes() {
long x = 0;
for(PrioQueue pq : queuesByPriority) {
if(pq.nonEmptyItemsWithID != null)
for(PrioQueue.Items q : pq.nonEmptyItemsWithID)
for(MessageItem it : q.items)
x += it.getLength() + 2;
}
return x;
}
private synchronized void enqueuePrioritizedMessageItem(MessageItem addMe) {
//Assume it goes on the end, both the common case
short prio = addMe.getPriority();
queuesByPriority[prio].addLast(addMe);
if(addMe.sendLoadRT)
mustSendLoadRT = true;
if(addMe.sendLoadBulk)
mustSendLoadBulk = true;
}
/**
* like enqueuePrioritizedMessageItem, but adds it to the front of those in the same priority.
*
* WARNING: Pulling a message and then pushing it back will mess up the fairness
* between UID's send order. Try to avoid it.
*/
synchronized void pushfrontPrioritizedMessageItem(MessageItem addMe) {
//Assume it goes on the front
short prio = addMe.getPriority();
queuesByPriority[prio].addFirst(addMe);
if(addMe.sendLoadRT)
mustSendLoadRT = true;
if(addMe.sendLoadBulk)
mustSendLoadBulk = true;
}
public synchronized MessageItem[] grabQueuedMessageItems() {
int size = 0;
for(PrioQueue queue : queuesByPriority)
size += queue.size();
MessageItem[] output = new MessageItem[size];
int ptr = 0;
for(PrioQueue queue : queuesByPriority) {
ptr = queue.addTo(output, ptr);
queue.clear();
}
return output;
}
/**
* Get the time at which the next message must be sent. If any message is
* overdue, we will return a value less than now, which may not be completely
* accurate.
* @param t The current next urgent time. The return value will be no greater
* than this.
* @param returnIfBefore The current time. If the next urgent time is less than
* this we return immediately rather than computing an accurate past value.
* Set to Long.MAX_VALUE if you want an accurate value.
* @return The next urgent time, but can be too high if it is less than now.
*/
public synchronized long getNextUrgentTime(long t, long returnIfBefore) {
for(PrioQueue queue: queuesByPriority) {
t = Math.min(t, queue.getNextUrgentTime(t, returnIfBefore));
if(t <= returnIfBefore) return t; // How much in the past doesn't matter, as long as it's in the past.
}
return t;
}
/**
* Returns <code>true</code> if there are messages that will timeout before
* <code>now</code>.
* @param now the timeout for messages waiting to be sent
* @return <code>true</code> if there are messages that will timeout before
* <code>now</code>
*/
public boolean mustSendNow(long now) {
return getNextUrgentTime(Long.MAX_VALUE, now) <= now;
}
/**
* Returns <code>true</code> if <code>minSize</code> + the length of all
* messages in this queue is greater than <code>maxSize</code>.
* @param minSize the starting size
* @param maxSize the maximum size
* @return <code>true</code> if <code>minSize</code> + the length of all
* messages in this queue is greater than <code>maxSize</code>
*/
public synchronized boolean mustSendSize(int minSize, int maxSize) {
int length = minSize;
for(PrioQueue items : queuesByPriority) {
length = items.addSize(length, maxSize);
if(length > maxSize) return true;
}
return false;
}
/** Grab a message to send. WARNING: PeerMessageQueue not only removes the message,
* it assumes it has been sent for purposes of fairness between UID's. You should try
* not to call this function if you are not going to be able to send the message:
* check in advance if possible. */
public synchronized MessageItem grabQueuedMessageItem(int minPriority) {
long now = System.currentTimeMillis();
MutableBoolean addPeerLoadStatsRT = new MutableBoolean();
MutableBoolean addPeerLoadStatsBulk = new MutableBoolean();
addPeerLoadStatsRT.value = true;
addPeerLoadStatsBulk.value = true;
for(int i=0;i<DMT.PRIORITY_REALTIME_DATA;i++) {
if(i < minPriority) continue;
if(logMINOR) Logger.minor(this, "Adding from priority "+i);
MessageItem ret = queuesByPriority[i].addPriorityMessages(now, addPeerLoadStatsRT, addPeerLoadStatsBulk);
if(ret != null) return ret;
}
// Include bulk or realtime, whichever is more urgent.
boolean tryRealtimeFirst = true;
// If one is empty, try the other.
// Otherwise try whichever is more urgent, favouring realtime if there is a draw.
// Realtime is supposed to be bursty.
if(queuesByPriority[DMT.PRIORITY_REALTIME_DATA].isEmpty()) {
tryRealtimeFirst = false;
} else if(queuesByPriority[DMT.PRIORITY_BULK_DATA].isEmpty()) {
tryRealtimeFirst = true;
} else if(queuesByPriority[DMT.PRIORITY_BULK_DATA].getNextUrgentTime(Long.MAX_VALUE, 0) >= queuesByPriority[DMT.PRIORITY_REALTIME_DATA].getNextUrgentTime(Long.MAX_VALUE, 0)) {
tryRealtimeFirst = true;
} else {
tryRealtimeFirst = false;
}
// FIXME token bucket?
if(tryRealtimeFirst) {
// Try realtime first
if(logMINOR) Logger.minor(this, "Trying realtime first");
MessageItem ret = queuesByPriority[DMT.PRIORITY_REALTIME_DATA].addPriorityMessages(now, addPeerLoadStatsRT, addPeerLoadStatsBulk);
if(ret != null) return ret;
if(logMINOR) Logger.minor(this, "Trying bulk");
ret = queuesByPriority[DMT.PRIORITY_BULK_DATA].addPriorityMessages(now, addPeerLoadStatsRT, addPeerLoadStatsBulk);
if(ret != null) return ret;
} else {
// Try bulk first
if(logMINOR) Logger.minor(this, "Trying bulk first");
MessageItem ret = queuesByPriority[DMT.PRIORITY_BULK_DATA].addPriorityMessages(now, addPeerLoadStatsRT, addPeerLoadStatsBulk);
if(ret != null) return ret;
if(logMINOR) Logger.minor(this, "Trying realtime");
ret = queuesByPriority[DMT.PRIORITY_REALTIME_DATA].addPriorityMessages(now, addPeerLoadStatsRT, addPeerLoadStatsBulk);
if(ret != null) return ret;
}
for(int i=DMT.PRIORITY_BULK_DATA+1;i<DMT.NUM_PRIORITIES;i++) {
if(i < minPriority) continue;
if(logMINOR) Logger.minor(this, "Adding from priority "+i);
MessageItem ret = queuesByPriority[i].addPriorityMessages(now, addPeerLoadStatsRT, addPeerLoadStatsBulk);
if(ret != null) return ret;
}
// Nothing to send.
return null;
}
public boolean removeMessage(MessageItem message) {
synchronized(this) {
short prio = message.getPriority();
if(!queuesByPriority[prio].removeMessage(message)) return false;
}
message.onFailed();
return true;
}
public synchronized void removeUIDsFromMessageQueues(Long[] list) {
for(PrioQueue queue : queuesByPriority) {
queue.removeUIDs(list);
}
}
}