package org.limewire.nio;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* A throttle that can be applied to non-blocking reads and writes.
* <p>
* Throttles work by giving amounts to interested parties in a First in, First
* out (FIFO) queue, ensuring that no one party uses all of the available
* bandwidth on every tick.
* <p>
* Listeners must adhere to the following contract in order for the
* <code>Throttle</code> to be effective.
<p>
* In order:
<ol>
* <li>When a listener wants to write, it must interest ONLY the Throttle.</li>
*<p>
* Call: Throttle.interest(ThrottleListener)
*<p>
* <li>When the Throttle informs the listener that bandwidth is available, it must interest
* the next party in the chain (ultimately the Socket).</li>
*<p>
* Callback: {@link ThrottleListener#bandwidthAvailable()}
*<p>
* <li>The listener must request data prior to writing (in response to requestBandwidth),
* and write out only the amount that it requested.</li>
*<p>
* Callback: ThrottleListener.requestBandwidth<br>
* Call: Throttle.request()
*<p>
* <li>The listener must release data (in response to releaseBandwidth) that it was given
* from a request but did not write.</li>
*<p>
* Callback: ThrottleListener.releaseBandwidth<br>
* Call: Throttle.release(amount)
*<p>
*</ol>
* <b>Extraneous</b>: The ThrottleListener must have an 'attachment' set that
* is the same attachment as the one used on the {@link SelectionKey} for the
* {@link SelectableChannel}. This is necessary so that <code>Throttle</code>
* can match up <code>SelectionKey</code> ready events with
* <code>ThrottleListener</code> interest.
* <p>
* The flow of a <code>Throttle</code> works like:
<pre>
*
* <b>Throttle</b> <b>ThrottleListener</b> <b>NIODispatcher</b>
*
* 1) Throttle.interest
* 2) <adds to request list>
* 3) Throttle.tick
* 4) ThrottleListener.bandwidthAvailable
* 5) SocketChannel.interest
* 6) <moves from request to interest list>
* 7) Selector.select
* 8) Throttle.selectableKeys
* 9) ThrottleListener.requestBandwidth
* 10) Throttle.request
* 11) SocketChannel.write [or SocketChannel.read]
* 12) ThrottleListener.releaseBandwidth
* 13) Throttle.release
* 14) <remove from interest>
</pre>
* If there are multiple listeners, steps 4 and 5 are repeated for each request, and steps 9 through 14
* are performed on interested parties until there is no bandwidth available. Another tick will
* generate more bandwidth, which will allow previously interested parties to request/write/release.
* Because interested parties are processed in FIFO order, all parties receive equal access to
* the bandwidth.
* <p>
* Note that due to the nature of <code>Throttle</code> and {@link NIODispatcher},
* ready parties may be told to <code>WriteObserver.handleWrite()</code> twice during each
* selection event. The latter will always return 0 to a request.
*/
public class NBThrottle implements Throttle {
//private static final Log LOG = LogFactory.getLog(NBThrottle.class);
private static final int DEFAULT_TICK_TIME = 100;
/** The number of milliseconds in each tick. */
private final int MILLIS_PER_TICK;
/** The maximum amount to ever give anyone. */
private final int MAXIMUM_TO_GIVE;
/** The minimum amount to ever give anyone. */
private final int MINIMUM_TO_GIVE;
/** Whether or not this throttle is for writing. (If false, it's for reading.) */
private final boolean _write;
/** The op that this uses when processing. */
private final int _processOp;
/** The amount that is available every tick. */
private volatile int _bytesPerTick;
/** The amount currently available in this tick. */
private volatile int _available;
/** The next time a tick should occur. */
private long _nextTickTime = -1;
/**
* A list of ThrottleListeners that are interested in bandwidthAvailable events.
* <p>
* As ThrottleListeners interest themselves interest themselves for writing,
* the requests are queued up here. When bandwidth is available the request is
* moved over to 'interested' after informing the ThrottleListener that bandwidth
* is available. New ThrottleListeners should not be added to this if they are
* already in interested.
*/
private Set<ThrottleListener> _requests = new HashSet<ThrottleListener>();
/**
* Attachments that are interested -> ThrottleListener that owns the attachment.
* <p>
* As new items become interested, they are added to the bottom of the set.
* When something is written, so long as it writes > 0, it is removed from the
* list (and put back at the bottom).
*/
private Map<Object, ThrottleListener> _interested = new LinkedHashMap<Object, ThrottleListener>();
/**
* Attachments that are ready-op'd.
* <p>
* This is temporary per each selectableKeys call, but is cached to avoid regenerating
* each time.
*/
private Map<Object, SelectionKey> _ready = new HashMap<Object, SelectionKey>();
/** Whether or not we're currently active in the selectableKeys portion. */
private boolean _active = false;
/**
* Constructs a throttle using the default values for latency and availability.
*/
public NBThrottle(boolean forWriting, float bytesPerSecond) {
this(forWriting, bytesPerSecond, true, DEFAULT_TICK_TIME);
}
/**
* Constructs a throttle that is either for reading or reading with the
* maximum <code>bytesPerSecond</code>.
* <p>
* The <code>Throttle</code> is tuned to expect 'maxRequestors' requesting
* data, allowing only the 'maxLatency' delay between serviced requests
* for any given requestor.
* <p>
* The values are only recommendations and may be ignored (within limits) by the
* <code>Throttle</code> in order to ensure that the <code>Throttle</code>
* behaves correctly.
*/
public NBThrottle(boolean forWriting, float bytesPerSecond, int maxRequestors, int maxLatency) {
this(forWriting, bytesPerSecond, true, maxRequestors == 0 ? DEFAULT_TICK_TIME : maxLatency / maxRequestors);
}
/**
* Constructs a new Throttle that is either for writing or reading, allowing
* the given bytesPerSecond.
* <p>
* Use 'true' for writing, 'false' for reading.
* <p>
* If addToDispatcher is false, NIODispatcher is not notified about the Throttle,
* so it will not be automatically ticked or told of selectable keys.
* <p>
* The throttle will allow bandwidth spreading every millisPerTick, after
* enforcing it's between 50 & 100.
*/
protected NBThrottle(boolean forWriting, float bytesPerSecond,
boolean addToDispatcher, int millisPerTick) {
MILLIS_PER_TICK = Math.min(100, Math.max(50,millisPerTick));
int ticksPerSecond = 1000 / MILLIS_PER_TICK;
_write = forWriting;
_processOp = forWriting ? SelectionKey.OP_WRITE : SelectionKey.OP_READ;
_bytesPerTick = (int)(bytesPerSecond / ticksPerSecond);
if(addToDispatcher)
NIODispatcher.instance().addThrottle(this);
if(forWriting) {
MAXIMUM_TO_GIVE = 1400;
MINIMUM_TO_GIVE = 30;
} else {
MAXIMUM_TO_GIVE = Integer.MAX_VALUE;
MINIMUM_TO_GIVE = 1;
}
}
public void setRate(float bytesPerSecond) {
int ticksPerSecond = 1000 / MILLIS_PER_TICK;
_bytesPerTick = (int)(bytesPerSecond / ticksPerSecond);
}
/**
* Notification from the NIODispatcher that a bunch of keys are now selectable.
*/
void selectableKeys(Collection<? extends SelectionKey> keys) {
if(_available >= MINIMUM_TO_GIVE && !_interested.isEmpty()) {
for(Iterator<? extends SelectionKey> i = keys.iterator(); i.hasNext(); ) {
SelectionKey key = i.next();
try {
if(key.isValid() && (_write ? key.isWritable() : key.isReadable())) {
Object attachment = NIODispatcher.instance().attachment(key.attachment());
if(_interested.containsKey(attachment)) {
//LOG.debug("Adding: " + attachment + " to ready");
_ready.put(attachment, key);
}
}
} catch(CancelledKeyException ignored) {
i.remove(); // it's cancelled, we can ignore it now & forever.
}
}
//LOG.trace("Interested: " + _interested.size() + ", ready: " + _ready.size());
_active = true;
long now = System.currentTimeMillis();
Iterator<Map.Entry<Object, ThrottleListener>> i = _interested.entrySet().iterator();
for(; i.hasNext(); ) {
Map.Entry<Object, ThrottleListener> next = i.next();
ThrottleListener listener = next.getValue();
Object attachment = next.getKey();
SelectionKey key = _ready.remove(attachment);
if(!listener.isOpen()) {
//LOG.trace("Removing closed but interested party: " + next.getKey());
i.remove();
} else if(key != null) {
i.remove();
//LOG.debug("Processing: " + key.attachment());
listener.requestBandwidth();
try {
NIODispatcher.instance().process(now, key, key.attachment(), _processOp);
} finally {
listener.releaseBandwidth();
}
if (_available < MINIMUM_TO_GIVE)
break;
}
}
_active = false;
}
}
/**
* Interests this <code>ThrottleListener</code> in being notified when
* bandwidth is available.
*/
public void interest(ThrottleListener writer) {
boolean wakeup;
synchronized(_requests) {
//LOG.debug("Adding: " + writer + " to requests");
wakeup = _requests.isEmpty();
_requests.add(writer);
}
if (wakeup || _available >= MINIMUM_TO_GIVE)
NIODispatcher.instance().wakeup();
}
/**
* Requests some bytes to write.
*/
public int request() {
if(!_active) // failsafe to ensure request only occurs when we want it
return 0;
int ret = Math.min(_available, MAXIMUM_TO_GIVE);
_available -= ret;
return ret;
}
/**
* Releases some unwritten bytes back to the available pool.
*/
public void release(int amount) {
if(_active) // failsafe to ensure releasing only occurs when we want it
_available += amount;
//LOG.trace("RETR: " + amount + ", REMAINING: " + _available + ", ALL: " + wroteAll + ", FROM: " + attachment);
}
/**
* Notification from <code>NIODispatcher</code> that some time has passed.
* <p>
* Returns <code>true</code> if all requests were satisfied. Returns
* <code>false</code> if there are still some requests that require further
* tick notifications.
*/
void tick(long currentTime) {
if(currentTime >= _nextTickTime) {
_available = _bytesPerTick;
_nextTickTime = currentTime + MILLIS_PER_TICK;
spreadBandwidth();
} else if(_available >= MINIMUM_TO_GIVE) {
spreadBandwidth();
}
}
public long nextTickTime() {
synchronized(_requests) {
if (_requests.isEmpty() && _interested.isEmpty())
return Long.MAX_VALUE;
}
return _nextTickTime;
}
/**
* Notifies all requestors that bandwidth is available.
*/
private void spreadBandwidth() {
synchronized(_requests) {
if(!_requests.isEmpty()) {
for(ThrottleListener req : _requests) {
Object attachment = req.getAttachment();
if(attachment == null)
throw new IllegalStateException("must have an attachment - listener: " + req);
//LOG.debug("Moving: " + attachment + " from requests to interested");
if(req.bandwidthAvailable())
_interested.put(attachment, req);
// else it'll be cleared when we loop later on.
}
_requests.clear();
}
}
}
}