package com.limegroup.gnutella.uploader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.collection.MultiIterable;
import org.limewire.collection.NumericBuffer;
import org.limewire.collection.QueueCounter;
import org.limewire.core.settings.UploadSettings;
import org.limewire.inspection.Inspectable;
import org.limewire.inspection.InspectionPoint;
import org.limewire.inspection.DataCategory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.limegroup.gnutella.InsufficientDataException;
/**
* This class implements the logic of managing BT uploads and HTTP Uploads.
* More information available see
* http://wiki.limewire.org/index.php?title=UploadSlotsAndBT
*/
@Singleton
public class UploadSlotManagerImpl implements UploadSlotManager {
private static final Log LOG = LogFactory.getLog(UploadSlotManagerImpl.class);
/**
* The three priority levels
*/
private static final int BT_SEED = 0; // low priority
private static final int HTTP = 1; // medium priority
private static final int HIGH = 2; // torrent downloads and metafiles.
/**
* The desired minimum quality of service to provide for uploads, in
* B/ms
*/
private static final float MINIMUM_UPLOAD_SPEED = 3.0f;
/**
* The list of active upload slot requests
* INVARIANT: sorted by priority and contains only
* requests of the highest priority or non-preemptible requests
*/
@InspectionPoint("active uploads")
private final CountingList <UploadSlotRequest> active;
/**
* The list of queued non-resumable requests
*/
@InspectionPoint(value = "queued uploads", category = DataCategory.USAGE)
private final CountingList <HTTPSlotRequest> queued;
/**
* The list of queued resumable requests
* (currently only Seeding BT Uploaders)
*/
@InspectionPoint(value = "queued resumable uploads", category = DataCategory.USAGE)
private final CountingList<BTSlotRequest> queuedResumable;
private final MultiIterable<UploadSlotRequest> allRequests;
private final NumericBuffer<Float> bandwidth = new NumericBuffer<Float>(10);
private float sessionAverage;
private int numMeasures;
@Inject
public UploadSlotManagerImpl() {
active = new CountingList<UploadSlotRequest>();
queued = new CountingList<HTTPSlotRequest>();
queuedResumable = new CountingList<BTSlotRequest>();
allRequests = new MultiIterable<UploadSlotRequest>(active, queued, queuedResumable);
}
/**
* Polls for an available upload slot. (HTTP-style)
*
* @param user the user that will use the upload slot
* @param queue if the user can enter the queue
* @param highPriority if the user needs an upload slot now or never
* @return the position in the queue if queued, -1 if rejected,
* 0 if it can proceed immediately
*/
public int pollForSlot(UploadSlotUser user, boolean queue, boolean highPriority) {
if (LOG.isDebugEnabled())
LOG.debug(user+" polling for slot, queuable "+queue);
return requestSlot(new HTTPSlotRequest(user, queue, highPriority));
}
/**
* Requests an upload slot. (BT-style)
*
* @param listener the listener that should be notified when a slot
* becomes available
* @param highPriority if the user needs an upload slot now or never
* @return the position of the upload if queued, -1 if rejected, 0 if
* it can proceed immediately.
*/
public int requestSlot(UploadSlotListener listener, boolean highPriority) {
if (LOG.isDebugEnabled())
LOG.debug(listener+" requesting slot, high priority "+highPriority);
return requestSlot(new BTSlotRequest(listener, highPriority));
}
private synchronized int requestSlot(UploadSlotRequest request) {
// see if there exists an uploader with higher priority
boolean existHigherPriority = existActiveHigherPriority(request.getPriority());
// see if this is already in the queue
int positionInQueue = positionInQueue(request);
// see if there are any uploaders with lower priority
int freeableSlots = getPreemptible(request.getPriority());
// if there is a higher priority upload or not enough free slots, queue.
if (existHigherPriority ||
!hasFreeSlot(active.size() +
Math.max(0,positionInQueue) -
freeableSlots)) {
if (!request.isQueuable())
return -1;
if (positionInQueue >= 0)
return ++positionInQueue;
else
return queueRequest(request);
}
// free any freeable slots
if (freeableSlots > 0)
killPreemptible(request.getPriority());
// remove from queue if it was there
if (positionInQueue > -1)
removeIfQueued(request.getUser());
addActiveRequest(request);
return 0;
}
/**
* @return the position in the appropriate queue of the request
* 0 if not in the queue
*/
private int positionInQueue(UploadSlotRequest request) {
return getQueue(request.getUser()).indexOf(request);
}
public synchronized int positionInQueue(UploadSlotUser user) {
List<? extends UploadSlotRequest> queue = getQueue(user);
for(int i = 0; i < queue.size();i++) {
UploadSlotRequest request = queue.get(i);
if (request.getUser() == user)
return i;
}
return -1;
}
/**
* @return the queue where requests from the user would be found.
*/
private List<? extends UploadSlotRequest> getQueue(UploadSlotUser user) {
return user instanceof UploadSlotListener ? queuedResumable : queued;
}
/**
* @return if there are any active users with higher priority
*/
private boolean existActiveHigherPriority(int priority) {
if (priority == HIGH)
return false;
if (!active.isEmpty()) {
UploadSlotRequest max = active.get(0);
if (max.getPriority() > priority)
return true;
}
return false;
}
/**
* @return the number of active uploaders with lower priority
* that can be preempted.
*/
private int getPreemptible(int priority) {
if (priority == BT_SEED)
return 0;
// iterate backwards
int ret = 0;
for(int i = active.size() - 1; i >= 0; i--) {
UploadSlotRequest request = active.get(i);
if (request.getPriority() < priority && request.isPreemptible())
ret++;
}
return ret;
}
/**
* Kills any active uploaders that can be preempted and have lower priority.
*/
private void killPreemptible(int priority) {
for(int i = active.size() - 1; i >= 0; i--) {
UploadSlotRequest request = active.get(i);
if (request.getPriority() < priority && request.isPreemptible()) {
if (LOG.isDebugEnabled())
LOG.debug("freeing slot from "+request.getUser());
active.remove(i);
request.getUser().releaseSlot();
}
}
}
/**
* @return whether there is a free slot for an HTTP uploader.
*/
public synchronized boolean hasHTTPSlot(int current) {
// This ignores currently active BT_SEED uploaders since they
// can be preempted.
if (existActiveHigherPriority(HTTP))
return false;
return hasFreeSlot(current);
}
/**
* @return true if there may be a free slot for an HTTP uploader.
*/
public synchronized boolean hasHTTPSlotForMeta(int current) {
return hasFreeSlot(current);
}
/**
* @return whether there would be a free slot if current many were taken.
*/
private boolean hasFreeSlot(int current) {
//Allow another upload if (a) we currently have fewer than
//SOFT_MAX_UPLOADS uploads or (b) some upload has more than
//MINIMUM_UPLOAD_SPEED KB/s. But never allow more than MAX_UPLOADS.
//
//In other words, we continue to allow uploads until everyone's
//bandwidth is diluted. The assumption is that with MAX_UPLOADS
//uploads, the probability that all just happen to have low capacity
//(e.g., modems) is small. This reduces "Try Again Later"'s at the
//expensive of quality, making swarmed downloads work better.
if (current >= UploadSettings.HARD_MAX_UPLOADS.getValue())
return false;
else if (current < UploadSettings.SOFT_MAX_UPLOADS.getValue())
return true;
else {
float fastest = 0f;
for (UploadSlotRequest request : active) {
UploadSlotUser user = request.getUser();
float speed = 0;
user.measureBandwidth();
try {
speed = user.getMeasuredBandwidth();
} catch (InsufficientDataException ide) {}
fastest = Math.max(fastest,speed);
if (fastest > MINIMUM_UPLOAD_SPEED)
return true;
}
return false;
}
}
public synchronized void measureBandwidth() {
float bw = getTotalBandwidth();
sessionAverage = ((sessionAverage * numMeasures) + bw) / (++numMeasures);
bandwidth.add(bw);
}
public synchronized float getMeasuredBandwidth() throws InsufficientDataException {
if (bandwidth.size() < bandwidth.getCapacity())
throw new InsufficientDataException();
return bandwidth.average().floatValue();
}
public synchronized float getAverageBandwidth() {
return sessionAverage;
}
private float getTotalBandwidth() {
float ret = 0;
for (UploadSlotRequest request : active) {
UploadSlotUser user = request.getUser();
user.measureBandwidth();
try {
ret += user.getMeasuredBandwidth();
} catch (InsufficientDataException ide) {}
}
return ret;
}
/**
* Adds a request to the appropriate queue if not already there.
* @return the position in the queue (>= 1)
*/
@SuppressWarnings("unchecked")
private <Request_t extends UploadSlotRequest>int queueRequest(Request_t request) {
List<Request_t> queue = (List<Request_t>)getQueue(request.user);
if (queue.size() == UploadSettings.UPLOAD_QUEUE_SIZE.getValue())
return -1;
queue.add(request);
if (LOG.isDebugEnabled())
LOG.debug("queued "+request.getUser()+" at position "+queue.size());
return queue.size();
}
/**
* Adds an active request.
*/
private void addActiveRequest(UploadSlotRequest request) {
int i = 0;
for(; i < active.size(); i++) {
UploadSlotRequest current = active.get(i);
if (current.getPriority() < request.getPriority())
break;
}
if (LOG.isDebugEnabled())
LOG.debug("added active request "+request.getUser()+" at position "+i);
active.add(i,request);
}
/**
* Cancels the request issued by this UploadSlotListener.
*/
public synchronized void cancelRequest(UploadSlotUser user) {
if (LOG.isDebugEnabled())
LOG.debug(user +" cancelling request");
if (!removeIfQueued(user))
requestDone(user);
}
/**
* Removes an UploadSlotUser from the queue.
* @return if the user was in the queue.
*/
private boolean removeIfQueued(UploadSlotUser user) {
List<? extends UploadSlotRequest> queue = getQueue(user);
for (Iterator<? extends UploadSlotRequest> iter = queue.iterator(); iter.hasNext();) {
UploadSlotRequest request = iter.next();
if (request.getUser() == user) {
iter.remove();
if (LOG.isDebugEnabled())
LOG.debug("remove queued request by "+user);
return true;
}
}
return false;
}
/**
* Notification that the UploadSlotUser is done with its request.
*/
public synchronized void requestDone(UploadSlotUser user) {
for (Iterator<? extends UploadSlotRequest> iter = active.iterator(); iter.hasNext();) {
UploadSlotRequest request = iter.next();
if (request.getUser() == user) {
if (LOG.isDebugEnabled())
LOG.debug("request finished for "+user);
iter.remove();
resumeQueued();
return;
}
}
}
/**
* Resumes an uploader from the resumable queue
* (in this specific case a Seeding BT uploader).
*/
private void resumeQueued() {
// can't resume if someone is still active
if (existActiveHigherPriority(BT_SEED))
return;
// consider moving this to an external collection
for(Iterator<BTSlotRequest> iter = queuedResumable.iterator(); iter.hasNext() && hasFreeSlot(active.size());) {
BTSlotRequest queuedRequest = iter.next();
iter.remove();
if (LOG.isDebugEnabled())
LOG.debug("resuming queued request "+queuedRequest.getUser());
active.add(queuedRequest);
queuedRequest.getListener().slotAvailable();
}
}
public synchronized int getNumActive() {
return active.size();
}
public synchronized int getNumQueued() {
return queued.size();
}
public synchronized int getNumQueuedResumable() {
return queuedResumable.size();
}
public synchronized int getNumUsersForHost(String host) {
int ret = 0;
for(UploadSlotRequest request : allRequests) {
if (host.equals(request.getUser().getHost()))
ret++;
}
return ret;
}
@Override
public synchronized String toString() {
StringBuilder ret = new StringBuilder();
ret.append("active:");
appendPriorities(active, ret);
ret.append("queued:");
appendPriorities(queued, ret);
ret.append("resumable:");
appendPriorities(queuedResumable, ret);
ret.append("bw now:").append(getTotalBandwidth());
ret.append(" session avg:").append(sessionAverage);
return ret.toString();
}
private void appendPriorities(List<? extends UploadSlotRequest> l, StringBuilder dest) {
int [] priorities = countPriorities(l);
for (int i = 0; i < priorities.length; i++)
dest.append(i).append(":").append(priorities[i]).append(" ");
}
private int[] countPriorities(List<? extends UploadSlotRequest> l) {
int [] ret = new int[3];
for (UploadSlotRequest r : l)
ret[r.getPriority()]++;
return ret;
}
/**
* A request for an upload slot.
*/
private abstract class UploadSlotRequest {
private final UploadSlotUser user;
private final boolean preempt;
private final int priority;
boolean isPreemptible() {
return preempt;
}
int getPriority() {
return priority;
}
UploadSlotUser getUser() {
return user;
}
abstract boolean isQueuable();
protected UploadSlotRequest(UploadSlotUser listener,
boolean preempt,
int priority) {
this.user = listener;
this.preempt = preempt;
this.priority = priority;
}
@Override
public boolean equals(Object o) {
if (! (o instanceof UploadSlotRequest))
return false;
UploadSlotRequest other = (UploadSlotRequest) o;
// one request per user at a time.
return getUser() == other.getUser();
}
@Override
public String toString() {
return getClass().getName() + "[user=" + user + "]";
}
}
/**
* An HTTP request for an upload slot.
*/
private class HTTPSlotRequest extends UploadSlotRequest {
private final boolean queuable;
HTTPSlotRequest (UploadSlotUser user, boolean queuable, boolean highPriority) {
super(user, false, highPriority ? HIGH : HTTP);
this.queuable = queuable;
}
@Override
boolean isQueuable() {
return queuable;
}
}
/**
* A BT request for an upload slot.
*/
private class BTSlotRequest extends UploadSlotRequest {
BTSlotRequest(UploadSlotListener listener, boolean highPriority) {
super(listener, !highPriority, highPriority ? HIGH : BT_SEED);
}
UploadSlotListener getListener() {
return (UploadSlotListener) getUser();
}
@Override
boolean isQueuable() {
return getPriority() == BT_SEED;
}
}
/**
* An ArrayList that keeps some stats on its elements.
*/
private static class CountingList<E> extends ArrayList<E>
implements Inspectable {
private final QueueCounter counter = new QueueCounter(10);
private volatile int maxSize;
private volatile long lastMod;
@Override
public Object inspect() {
Map<String,Number> ret = new HashMap<String, Number>();
ret.put("avg", counter.getAverageSize());
ret.put("max", maxSize);
ret.put("cur", size());
ret.put("mod", System.currentTimeMillis() - lastMod);
return ret;
}
@Override
public boolean add(E e) {
lastMod = System.currentTimeMillis();
counter.recordArrival();
maxSize = Math.max(maxSize, 1 + size());
return super.add(e);
}
@Override
public void add(int index, E e) {
lastMod = System.currentTimeMillis();
counter.recordArrival();
maxSize = Math.max(maxSize, 1 + size());
super.add(index, e);
}
@Override
public E remove(int index) {
lastMod = System.currentTimeMillis();
counter.recordDeparture();
return super.remove(index);
}
@Override
public boolean remove(Object e) {
boolean ret = super.remove(e);
if (ret) {
lastMod = System.currentTimeMillis();
counter.recordDeparture();
}
return ret;
}
}
public synchronized void cleanup() {
active.clear();
queued.clear();
queuedResumable.clear();
}
}