package com.limegroup.gnutella.io;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Collection;
import java.util.Iterator;
import java.nio.channels.SelectionKey;
import java.nio.channels.CancelledKeyException;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
/**
* A throttle that can be applied to non-blocking reads & writes.
*
* Throttles work by giving amounts to interested parties in a FIFO
* queue, ensuring that no one party uses all of the available bandwidth
* on every tick.
*
* Listeners must adhere to the following contract in order for the Throttle to be effective.
* In order:
* 1) When a listener wants to write, it must interest ONLY the Throttle.
* Call: Throttle.interest(listener)
*
* 2) When the throttle informs the listener that bandwidth is available, it must interest
* the next party in the chain (ultimately the Socket).
* Callback: ThrottleListener.bandwidthAvailable()
*
* 3) The listener must request data prior to writing, and write out only the amount
* that it requested.
* Call: Throttle.request()
*
* 4) The listener must release data that it was given from a request but did not write.
* Call: Throttle.release(amount)
*
* Extraneous: The ThrottleListener must have an 'attachment' set that is the same attachment
* as the one used on the SelectionKey for the SelectableChannel. This is
* necessary so that Throttle can match up SelectionKey ready events
* with ThrottleListener interest.
*
* The flow of a Throttle works like:
* Throttle ThrottleListener NIODispatcher
* 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) Throttle.request
* 10) SocketChannel.write
* 11) Throttle.release
* 12) <remove from interest>
*
* If there are multiple listeners, steps 4 & 5 are repeated for each request, and steps 9 through 12
* 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 will receive equal access to
* the bandwidth.
*
* Note that due to the nature of Throttle & NIODispatcher, ready parties may be told to handleWrite
* 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);
/** The maximum amount to ever give anyone. */
private static final int MAXIMUM_TO_GIVE = 1400;
/** The minimum amount to ever give anyone. */
private static final int MINIMUM_TO_GIVE = 30;
private static final int DEFAULT_TICK_TIME = 100;
/** The number of milliseconds in each tick. */
private final int MILLIS_PER_TICK;
/** 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 int _available;
/** The next time a tick should occur. */
private long _nextTickTime = -1;
/**
* A list of ThrottleListeners that are interested in bandwidthAvailable events.
*
* 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 /* of ThrottleListener */ _requests = new HashSet();
/**
* Attachments that are interested -> ThrottleListener that owns the attachment.
*
* 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 /* of Object (ThrottleListener.getAttachment()) -> ThrottleListener */ _interested = new LinkedHashMap();
/**
* Attachments that are ready-op'd.
*
* This is temporary per each selectableKeys call, but is cached to avoid regenerating
* each time.
*/
private Map /* of Object (ThrottleListener.getAttachment()) */ _ready = new HashMap();
/** Whether or not we're currently active in the selectableKeys portion. */
private boolean _active = false;
/**
* Constructs a throttle using the default values for latency & 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 bytesPerSecond.
*
* The Throttle is tuned to expect 'maxRequestors' requesting data, allowing only the 'maxLatency'
* delay between serviced requests for any given requestor.
*
* The values are only recommendations and may be ignored (within limits) by the Throttle
* in order to ensure that the Throttle 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.
*
* Use 'true' for writing, 'false' for reading.
*
* If addToDispatcher is false, NIODispatcher is not notified about the Throttle,
* so it will not be automatically ticked or told of selectable keys.
*
* 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 / millisPerTick;
_write = forWriting;
_processOp = forWriting ? SelectionKey.OP_WRITE : SelectionKey.OP_READ;
_bytesPerTick = (int)((float)bytesPerSecond / ticksPerSecond);
if(addToDispatcher)
NIODispatcher.instance().addThrottle(this);
}
/**
* Notification from the NIODispatcher that a bunch of keys are now selectable.
*/
void selectableKeys(Collection /* of SelectionKey */ keys) {
if(_available >= MINIMUM_TO_GIVE && !_interested.isEmpty()) {
for(Iterator i = keys.iterator(); i.hasNext(); ) {
SelectionKey key = (SelectionKey)i.next();
try {
if(key.isValid() && (_write ? key.isWritable() : key.isReadable())) {
Object attachment = NIODispatcher.instance().attachment(key.attachment());
if(_interested.containsKey(attachment))
_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();
for(Iterator i = _interested.entrySet().iterator(); !_ready.isEmpty() && i.hasNext(); ) {
Map.Entry next = (Map.Entry)i.next();
ThrottleListener listener = (ThrottleListener)next.getValue();
Object attachment = next.getKey();
SelectionKey key = (SelectionKey)_ready.remove(attachment);
if(!listener.isOpen()) {
//LOG.trace("Removing closed but interested party: " + next.getKey());
i.remove();
} else if(key != null) {
NIODispatcher.instance().process(now, key, key.attachment(), _processOp);
i.remove();
if(_available < MINIMUM_TO_GIVE)
break;
}
}
_active = false;
}
}
/**
* Interests this ThrottleListener in being notified when bandwidth is available.
*/
public void interest(ThrottleListener writer) {
synchronized(_requests) {
_requests.add(writer);
}
}
/**
* Requests some bytes to write.
*/
public int request() {
if(!_active) // this is gonna happen from NIODispatcher's processing
return 0;
int ret = Math.min(_available, MAXIMUM_TO_GIVE);
_available -= ret;
//LOG.trace("GAVE: " + ret + ", REMAINING: " + _available + ", TO: " + attachment);
return ret;
}
/**
* Releases some unwritten bytes back to the available pool.
*/
public void release(int amount) {
_available += amount;
//LOG.trace("RETR: " + amount + ", REMAINING: " + _available + ", ALL: " + wroteAll + ", FROM: " + attachment);
}
/**
* Set the number of bytes to write per second
*
* @param bytesPerSecond
*/
public void limit(int bytesPerSecond) {
_bytesPerTick = (int)(bytesPerSecond * MILLIS_PER_TICK / 1000);
}
/**
* Notification from NIODispatcher that some time has passed.
*
* Returns true if all requests were satisifed. Returns false 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();
}
}
/**
* Notifies all requestors that bandwidth is available.
*/
private void spreadBandwidth() {
synchronized(_requests) {
if(!_requests.isEmpty()) {
for(Iterator i = _requests.iterator(); i.hasNext(); ) {
ThrottleListener req = (ThrottleListener)i.next();
Object attachment = req.getAttachment();
if(!_interested.containsKey(attachment)) {
if(req.bandwidthAvailable())
_interested.put(attachment, req);
// else it'll be cleared when we loop later on.
}
}
_requests.clear();
}
}
}
}