package edu.washington.cs.oneswarm.f2f.datagram; import java.util.LinkedList; import java.util.logging.Level; import java.util.logging.Logger; import org.gudy.azureus2.core3.util.Average; import org.gudy.azureus2.core3.util.Debug; import edu.washington.cs.oneswarm.f2f.datagram.DatagramConnection.DatagramSendThread; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessage; import edu.washington.cs.oneswarm.f2f.network.FriendConnection; public class DatagramRateLimitedChannelQueue extends DatagramRateLimiter { public final static Logger logger = Logger.getLogger(DatagramRateLimitedChannelQueue.class .getName()); // Just set to 1s for now, will be changed with pack pressure cc. private static final long MAX_DYN_QUEUE_LENGTH_MS = 1000; // average over 10s, update every 1s. private final Average uploadRate = Average.getInstance(1000, 10); private final DatagramSendThread sendThread; private final LinkedList<OSF2FMessage> messageQueue = new LinkedList<OSF2FMessage>(); private int queueLength = 0; private final static int BASE_MAX_QUEUE_LENGTH = 2 * DatagramConnection.MAX_DATAGRAM_SIZE; // Visible for testing int maxQueueLength = BASE_MAX_QUEUE_LENGTH; private long lastPacketSentAt = System.currentTimeMillis(); private final int channelId; public DatagramRateLimitedChannelQueue(int channelId, DatagramSendThread sendThread) { this.sendThread = sendThread; this.channelId = channelId; } @Override public void allocateTokens() { } @Override public synchronized int refillBucket(int tokens) { availableTokens += tokens; assert (availableTokens >= 0); if (logger.isLoggable(Level.FINEST)) { logger.finest(toString() + ": refilling " + tokens + " tokens, available=" + availableTokens); } // Send send(); // Return leftovers. if (availableTokens > maxAvailableTokens) { int overflow = availableTokens - maxAvailableTokens; logger.finest(toString() + ": overflow by " + overflow + "tokens, before: " + availableTokens + "/" + maxAvailableTokens); availableTokens = maxAvailableTokens; return tokens - overflow; } return tokens; } public synchronized void queuePacket(OSF2FMessage message) { int messageSize = getWireSize(message); if (messageSize + queueLength > maxQueueLength) { if (logger.isLoggable(Level.FINEST)) { logger.finest(toString() + ": dropping packet, packetSize=" + messageSize + " queueLength=" + queueLength + " max=" + maxQueueLength + ", packet:" + message.getDescription()); } message.destroy(); return; } queueLength += messageSize; messageQueue.add(message); send(); } public boolean isExpired() { return System.currentTimeMillis() - lastPacketSentAt > FriendConnection.OVERLAY_FORWARD_TIMEOUT; } public boolean isEmpty() { return queueLength == 0; } private int getWireSize(OSF2FMessage message) { int serializedByteNum = OSF2FMessage.MESSAGE_HEADER_LEN + message.getMessageSize(); int padding = DatagramEncrypter.calcPaddingLength(serializedByteNum); int wireSize = DatagramEncrypter.SEQUENCE_NUMBER_BYTES + serializedByteNum + padding + DatagramEncrypter.HMAC_KEY_LENGTH; return wireSize; } private void send() { // Only initiate if we have enough tokens to send a full packet if (availableTokens < DatagramConnection.MAX_DATAGRAM_SIZE) { logger.finest(toString() + ": not enough tokens available to send a packet"); return; } try { int bytes = 0; int packets = 0; while (sendOk()) { OSF2FMessage msg = messageQueue.removeFirst(); int messageSize = getWireSize(msg); packets++; availableTokens -= messageSize; queueLength -= messageSize; bytes += messageSize; sendThread.queueMessage(msg); } uploadRate.addValue(bytes); updateMaxQueueLength(); lastPacketSentAt = System.currentTimeMillis(); if (logger.isLoggable(Level.FINEST)) { logger.finest(toString() + ": sent: " + packets + " packets (" + bytes + " bytes), queue=" + queueLength + " rate=" + uploadRate.getAverage() + " max_queue=" + maxQueueLength + " tokens=" + availableTokens); if (messageQueue.size() > 0) { OSF2FMessage m = messageQueue.getFirst(); logger.finest(toString() + ": message queue: " + messageQueue.size() + "packets, space needed to send next: " + (getWireSize(m) - availableTokens)); } } } catch (InterruptedException e) { Debug.out(e); } } private boolean sendOk() { if (messageQueue.size() == 0) { return false; } return availableTokens >= getWireSize(messageQueue.getFirst()); } private void updateMaxQueueLength() { maxQueueLength = (int) Math.round(BASE_MAX_QUEUE_LENGTH + MAX_DYN_QUEUE_LENGTH_MS * uploadRate.getAverage() / 1000.0); } public synchronized void clear() { while (messageQueue.size() > 0) { OSF2FMessage message = messageQueue.removeLast(); message.destroy(); } } @Override public String toString() { return super.toString() + " Channel=" + getChannelId(); } public int getChannelId() { return channelId; } }