package com.limegroup.gnutella; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.HttpException; import org.apache.http.HttpInetConnection; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.nio.NHttpConnection; import org.apache.http.nio.protocol.NHttpRequestHandler; import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestHandler; import org.limewire.collection.Buffer; import org.limewire.collection.FixedsizeForgetfulHashMap; import org.limewire.core.settings.ConnectionSettings; import org.limewire.core.settings.UploadSettings; import org.limewire.http.HttpAcceptorListener; import org.limewire.http.auth.ServerAuthState; import org.limewire.inject.EagerSingleton; import org.limewire.lifecycle.Service; import org.limewire.lifecycle.ServiceRegistry; import org.limewire.util.FileLocker; import org.limewire.util.FileUtils; import org.limewire.util.Objects; import com.google.inject.Inject; import com.google.inject.Provider; import com.limegroup.gnutella.Uploader.UploadStatus; import com.limegroup.gnutella.auth.UrnValidator; import com.limegroup.gnutella.http.HttpContextParams; import com.limegroup.gnutella.library.FileDesc; import com.limegroup.gnutella.library.Library; import com.limegroup.gnutella.statistics.TcpBandwidthStatistics; import com.limegroup.gnutella.uploader.FileRequestHandler; import com.limegroup.gnutella.uploader.HTTPUploadSession; import com.limegroup.gnutella.uploader.HTTPUploadSessionManager; import com.limegroup.gnutella.uploader.HTTPUploader; import com.limegroup.gnutella.uploader.HttpRequestHandlerFactory; import com.limegroup.gnutella.uploader.UploadSlotManager; import com.limegroup.gnutella.uploader.UploadType; import com.limegroup.gnutella.uploader.authentication.GnutellaBrowseFileViewProvider; import com.limegroup.gnutella.uploader.authentication.GnutellaUploadFileViewProvider; /** * Manages {@link HTTPUploader} objects that are created by * {@link HttpRequestHandler}s through the {@link HTTPUploadSessionManager} * interface. Since HTTP 1.1 allows multiple requests for a single connection an * {@link HTTPUploadSession} is created for each connection. It keeps track of * queuing (which is per connection) and bandwidth and has a reference to the * {@link HTTPUploader} that represents the current request. * <p> * The state of <code>HTTPUploader</code> follows this pattern: * * <pre> * |->---- THEX_REQUEST ------->--| * |->---- UNAVAILABLE_RANGE -->--| * |->---- PUSH_PROXY --------->--| * /-->---- FILE NOT FOUND ----->--| * /--->---- MALFORMED REQUEST -->--| * /---->---- BROWSE HOST -------->--| * /----->---- UPDATE FILE -------->--| * /------>---- QUEUED ------------->--| * /------->---- LIMIT REACHED ------>--| * /-------->---- UPLOADING ---------->--| * -->--CONNECTING-->--/ | * | \|/ * | | * /|\ |--->INTERRUPTED * |--------<---COMPLETE-<------<-------<-------<------/ (done) * | * | * (done) * </pre> * * COMPLETE uploaders may be using HTTP/1.1, in which case the HTTPUploader * recycles back to CONNECTING upon receiving the next GET/HEAD request and * repeats. * <p> * INTERRUPTED HTTPUploaders are never reused. However, it is possible that the * socket may be reused. This case is only possible when a requester is queued * for one file and sends a subsequent request for another file. The first * <code>HTTPUploader</code> is set as interrupted and a second one is created * for the new file, using the same connection as the first one. * <p> * To initialize the upload manager {@link #start(HTTPAcceptor)} needs to be * invoked which registers handlers with an {@link HTTPAcceptor}. * * @see com.limegroup.gnutella.uploader.HTTPUploader * @see com.limegroup.gnutella.HTTPAcceptor */ @EagerSingleton public class HTTPUploadManager implements FileLocker, BandwidthTracker, UploadManager, HTTPUploadSessionManager, Service { /** The key used to store the {@link HTTPUploadSession} object. */ private final static String SESSION_KEY = "org.limewire.session"; private static final Log LOG = LogFactory.getLog(HTTPUploadManager.class); /** * This is a <code>List</code> of all of the current <code>Uploader</code> * instances (all of the uploads in progress that are not queued). */ private List<HTTPUploader> activeUploadList = new LinkedList<HTTPUploader>(); /** A manager for the available upload slots */ private final UploadSlotManager slotManager; /** Set to true when an upload has been successfully completed. */ private volatile boolean hadSuccesfulUpload = false; /** Number of force-shared active uploads. */ private int forcedUploads; private final HttpRequestHandler freeLoaderRequestHandler; private final ResponseListener responseListener = new ResponseListener(); /** * Number of active uploads that are not accounted in the slot manager but * whose bandwidth is counted. (i.e. Multicast) */ private final Set<HTTPUploader> localUploads = new CopyOnWriteArraySet<HTTPUploader>(); /** * LOCKING: obtain this' monitor before modifying any of the data structures */ /** * The number of uploads considered when calculating capacity, if possible. * BearShare uses 10. Settings it too low causes you to be fooled be a * streak of slow downloaders. Setting it too high causes you to be fooled * by a number of quick downloads before your slots become filled. */ private static final int MAX_SPEED_SAMPLE_SIZE = 5; /** * The min number of uploads considered to give out your speed. Same * criteria needed as for MAX_SPEED_SAMPLE_SIZE. */ private static final int MIN_SPEED_SAMPLE_SIZE = 5; /** The minimum number of bytes transferred by an uploadeder to count. */ private static final int MIN_SAMPLE_BYTES = 200000; // 200KB public static final int TRANSFER_SOCKET_TIMEOUT = 2 * 60 * 1000; /** The average speed in kiloBITs/second of the last few uploads. */ private Buffer<Integer> speeds = new Buffer<Integer>(MAX_SPEED_SAMPLE_SIZE); /** * The highestSpeed of the last few downloads, or -1 if not enough downloads * have been down for an accurate sample. INVARIANT: highestSpeed>=0 ==> * highestSpeed==max({i | i in speeds}) INVARIANT: speeds.size()<MIN_SPEED_SAMPLE_SIZE * <==> highestSpeed==-1 */ private volatile int highestSpeed = -1; /** * The number of measureBandwidth's we've had */ private int numMeasures = 0; /** * The current average bandwidth. * * This is only counted while uploads are active. */ private float averageBandwidth = 0f; /** The last value that getMeasuredBandwidth created. */ private volatile float lastMeasuredBandwidth; /** * Remembers uploaders to disadvantage uploaders that hammer us for download * slots. Stores up to 250 entries Maps IP String to RequestCache */ private final Map<String, RequestCache> REQUESTS = new FixedsizeForgetfulHashMap<String, RequestCache>( 250); private final Provider<ActivityCallback> activityCallback; private volatile boolean started; private final HttpRequestHandlerFactory httpRequestHandlerFactory; private final Provider<HTTPAcceptor> httpAcceptor; private final TcpBandwidthStatistics tcpBandwidthStatistics; private final Provider<GnutellaUploadFileViewProvider> gnutellaUploadFileListProvider; private final Provider<GnutellaBrowseFileViewProvider> gnutellaBrowseFileListProvider; private final UrnValidator urnValidator; private final Library library; @Inject public HTTPUploadManager(UploadSlotManager slotManager, HttpRequestHandlerFactory httpRequestHandlerFactory, Provider<HTTPAcceptor> httpAcceptor, Provider<ActivityCallback> activityCallback, TcpBandwidthStatistics tcpBandwidthStatistics, Provider<GnutellaUploadFileViewProvider> gnutellaFileListProvider, Provider<GnutellaBrowseFileViewProvider> gnutellaBrowseFileListProvider, UrnValidator urnValidator, Library library) { this.gnutellaUploadFileListProvider = gnutellaFileListProvider; this.gnutellaBrowseFileListProvider = gnutellaBrowseFileListProvider; this.slotManager = Objects.nonNull(slotManager, "slotManager"); this.httpRequestHandlerFactory = httpRequestHandlerFactory; this.freeLoaderRequestHandler = httpRequestHandlerFactory.createFreeLoaderRequestHandler(); this.httpAcceptor = Objects.nonNull(httpAcceptor, "httpAcceptor"); this.library = Objects.nonNull(library, "library"); this.activityCallback = Objects.nonNull(activityCallback, "activityCallback"); this.tcpBandwidthStatistics = Objects.nonNull(tcpBandwidthStatistics, "tcpBandwidthStatistics"); this.urnValidator = urnValidator; } public String getServiceName() { return org.limewire.i18n.I18nMarker.marktr("Upload Management"); } public void initialize() { } @Inject void register(ServiceRegistry registry) { registry.register(this); } /** * Registers the upload manager at <code>acceptor</code>. * * @throws IllegalStateException if uploadmanager was already started * @see #stop(HTTPAcceptor) */ public void start() { if (started) { throw new IllegalStateException(); } FileUtils.addFileLocker(this); httpAcceptor.get().addAcceptorListener(responseListener); // browse httpAcceptor.get().registerHandler("/", httpRequestHandlerFactory.createBrowseRequestHandler(gnutellaBrowseFileListProvider.get(), false)); // push-proxy requests NHttpRequestHandler pushProxyHandler = httpRequestHandlerFactory.createPushProxyRequestHandler(); httpAcceptor.get().registerHandler("/gnutella/push-proxy", pushProxyHandler); httpAcceptor.get().registerHandler("/gnet/push-proxy", pushProxyHandler); // uploads FileRequestHandler fileRequestHandler = httpRequestHandlerFactory.createFileRequestHandler(gnutellaUploadFileListProvider.get(), false); httpAcceptor.get().registerHandler("/uri-res/*", fileRequestHandler); started = true; } /** * Unregisters the upload manager at <code>acceptor</code>. * * @see #start(HTTPAcceptor) */ public void stop() { if (!started) { throw new IllegalStateException(); } httpAcceptor.get().unregisterHandler("/"); httpAcceptor.get().unregisterHandler("/update.xml"); httpAcceptor.get().unregisterHandler("/gnutella/push-proxy"); httpAcceptor.get().unregisterHandler("/gnet/push-proxy"); httpAcceptor.get().unregisterHandler("/get*"); httpAcceptor.get().unregisterHandler("/uri-res/*"); httpAcceptor.get().removeAcceptorListener(responseListener); FileUtils.removeFileLocker(this); started = false; } public void handleFreeLoader(HttpRequest request, HttpResponse response, HttpContext context, HTTPUploader uploader) throws HttpException, IOException { assert started; uploader.setState(UploadStatus.FREELOADER); freeLoaderRequestHandler.handle(request, response, context); } /** * Determines whether or not this uploader should bypass queuing, (meaning * that it will always work immediately, and will not use up slots for other * uploaders). */ private boolean shouldBypassQueue(HttpRequest request, HTTPUploader uploader) { assert uploader.getState() == UploadStatus.CONNECTING; return "HEAD".equals(request.getRequestLine().getMethod()) || uploader.isForcedShare(); } /** * Cleans up a finished uploader. This does the following: * <ol> * <li>Reports the speed at which this upload occured. * <li>Removes the uploader from the active upload list * <li>Increments the completed uploads in the FileDesc * <li>Removes the uploader from the GUI * </ol> */ public void cleanupFinishedUploader(HTTPUploader uploader) { assert started; if (LOG.isTraceEnabled()) LOG.trace("Cleaning uploader " + uploader); UploadStatus state = uploader.getState(); UploadStatus lastState = uploader.getLastTransferState(); // assertAsFinished(state); long finishTime = System.currentTimeMillis(); synchronized (this) { // Report how quickly we uploaded the data. if (uploader.getStartTime() > 0) { reportUploadSpeed(finishTime - uploader.getStartTime(), uploader.getTotalAmountUploaded()); } removeFromList(uploader); localUploads.remove(uploader); } if (uploader.getUploadType() != null && !uploader.getUploadType().isInternal()) { FileDesc fd = uploader.getFileDesc(); if (fd != null && state == UploadStatus.COMPLETE && (lastState == UploadStatus.UPLOADING || lastState == UploadStatus.THEX_REQUEST)) { fd.incrementCompletedUploads(); activityCallback.get().handleSharedFileUpdate(fd.getFile()); } } activityCallback.get().removeUpload(uploader); } public synchronized void addAcceptedUploader(HTTPUploader uploader, HttpContext context) { assert started; if (uploader.isForcedShare()) forcedUploads++; else if (HttpContextParams.isLocal(context)) localUploads.add(uploader); activeUploadList.add(uploader); uploader.setStartTime(System.currentTimeMillis()); } public void sendResponse(HTTPUploader uploader, HttpResponse response) { assert started; uploader.setLastResponse(response); if (uploader.isVisible()) { return; } // We are going to notify the gui about the new upload, and let // it decide what to do with it - will act depending on it's // state activityCallback.get().addUpload(uploader); uploader.setVisible(true); if (!uploader.getUploadType().isInternal()) { FileDesc fd = uploader.getFileDesc(); if (fd != null) { fd.incrementAttemptedUploads(); activityCallback.get().handleSharedFileUpdate(fd.getFile()); } } } public synchronized boolean isServiceable() { if (!started) { return false; } return slotManager.hasHTTPSlot(uploadsInProgress() + getNumQueuedUploads()); } public synchronized boolean mayBeServiceable() { if (!started) { return false; } return isServiceable(); } public synchronized int uploadsInProgress() { return activeUploadList.size() - forcedUploads; } public synchronized int getNumQueuedUploads() { return slotManager.getNumQueued(); } public boolean hadSuccesfulUpload() { return hadSuccesfulUpload; } public synchronized boolean isConnectedTo(InetAddress addr) { if (slotManager.getNumUsersForHost(addr.getHostAddress()) > 0) return true; for (HTTPUploader uploader : activeUploadList) { InetAddress host = uploader.getConnectedHost(); if (host != null && host.equals(addr)) return true; } return false; } public boolean releaseLock(File file) { assert started; FileDesc fd = library.getFileDesc(file); if (fd != null) return killUploadsForFileDesc(fd); else return false; } public synchronized boolean killUploadsForFileDesc(FileDesc fd) { boolean ret = false; for (HTTPUploader uploader : activeUploadList) { FileDesc upFD = uploader.getFileDesc(); if (upFD != null && upFD.equals(fd)) { ret = true; uploader.stop(); } } return ret; } /** * Checks whether the given upload may proceed based on number of slots, * position in upload queue, etc. Updates the upload queue as necessary. * * @return ACCEPTED if the download may proceed, QUEUED if this is in the * upload queue, REJECTED if this is flat-out disallowed (and hence * not queued) and BANNED if the downloader is hammering us, and * BYPASS_QUEUE if this is a File-View request that isn't hammering * us. If REJECTED, <code>uploader</code>'s state will be set to * LIMIT_REACHED. If BANNED, the <code>Uploader</code>'s state * will be set to BANNED_GREEDY. */ private synchronized QueueStatus checkAndQueue(HTTPUploadSession session, HttpContext context) { RequestCache rqc = REQUESTS.get(session.getHost()); if (rqc == null) rqc = new RequestCache(); // make sure we don't forget this RequestCache too soon! REQUESTS.put(session.getHost(), rqc); rqc.countRequest(); // only enforce hammering for unauthenticated clients that don't have credentials ServerAuthState serverAuthState = (ServerAuthState) context.getAttribute(ServerAuthState.AUTH_STATE); if (serverAuthState == null || serverAuthState.getCredentials() == null) { if (rqc.isHammering()) { if (LOG.isWarnEnabled()) LOG.warn("BANNED: " + session.getHost() + " (hammering)"); return QueueStatus.BANNED; } } else { if (LOG.isDebugEnabled()) { LOG.debug("friend asking: " + serverAuthState.getCredentials().getUserPrincipal().getName()); } } FileDesc fd = session.getUploader().getFileDesc(); URN sha1 = fd.getSHA1Urn(); if (!urnValidator.isValid(sha1)) { urnValidator.validate(sha1); } if (rqc.isDupe(sha1) && UploadSettings.CHECK_DUPES.getValue()) { if (LOG.isDebugEnabled()) LOG.debug("REJECTED: request " + sha1 + " from " + session.getHost() + " (duplicate request)"); return QueueStatus.REJECTED; } // check the host limit unless this is a poll if (slotManager.positionInQueue(session) == -1 && hostLimitReached(session.getHost())) { if (LOG.isDebugEnabled()) LOG.debug("REJECTED: request " + sha1 + " from " + session.getHost() + " (host limit reached)"); return QueueStatus.REJECTED; } int queued = slotManager.pollForSlot(session, session.getUploader() .supportsQueueing(), session.getUploader().isPriorityShare()); // if (LOG.isDebugEnabled()) // LOG.debug("queued at " + queued); if (queued == -1) { // not accepted nor queued. if (LOG.isDebugEnabled()) LOG.debug("QUEUED: request " + sha1 + " from " + session.getHost() + " (attempt to queue failed)"); return QueueStatus.REJECTED; } if (queued > 0 && session.poll()) { if (LOG.isDebugEnabled()) LOG.debug("BANNED: request " + sha1 + " from " + session.getHost()); slotManager.cancelRequest(session); // TODO we used to just drop the connection return QueueStatus.BANNED; } if (queued > 0) { if (LOG.isDebugEnabled()) LOG.debug("QUEUED: request " + sha1 + " from " + session.getHost()); return QueueStatus.QUEUED; } else { if (LOG.isDebugEnabled()) LOG.debug("ACCEPTED: request " + sha1 + " from " + session.getHost()); rqc.startedTransfer(sha1); return QueueStatus.ACCEPTED; } } /** * Decrements the number of active uploads for the host specified in the * <code>host</code> argument, removing that host from the * <code>Map</code> if this was the only upload allocated to that host. * <p> * This method also removes <code>uploader</code> from the * <code>List</code> of active uploads. */ private synchronized void removeFromList(HTTPUploader uploader) { // if the uploader is not in the active list, we should not // try remove the urn from the map of unique uploaded files for that // host. if (activeUploadList.remove(uploader)) { if (uploader.isForcedShare()) forcedUploads--; // at this point it is safe to allow other uploads from the same // host RequestCache rcq = REQUESTS.get(uploader.getHost()); // check for nulls so that unit tests pass if (rcq != null && uploader.getFileDesc() != null) rcq.transferDone(uploader.getFileDesc().getSHA1Urn()); } // Enable auto shutdown if (activeUploadList.size() == 0) activityCallback.get().uploadsComplete(); } /** * @return false if the number of uploads from the host is strictly LESS * than the MAX, although we want to allow exactly MAX uploads from * the same host. This is because this method is called BEFORE we * add/allow the. upload. */ private synchronized boolean hostLimitReached(String host) { return slotManager.getNumUsersForHost(host) >= UploadSettings.UPLOADS_PER_PERSON .getValue(); } // //////////////// Bandwidth Allocation and Measurement/////////////// /** * Calculates the appropriate burst size for the allocating bandwidth on the * upload. * * @return burstSize. if it is the special case, in which we want to upload * as quickly as possible. */ public int calculateBandwidth() { // public int calculateBurstSize() { float totalBandwith = getTotalBandwith(); float burstSize = totalBandwith / uploadsInProgress(); return (int) burstSize; } /** * @return the total bandwith available for uploads */ private float getTotalBandwith() { // To calculate the total bandwith available for // uploads, there are two properties. The first // is what the user *thinks* their connection // speed is. Note, that they may have set this // wrong, but we have no way to tell. float connectionSpeed = ConnectionSettings.CONNECTION_SPEED.getValue() / 8.0f; // the second number is the speed that they have // allocated to uploads. This is really a percentage // that the user is willing to allocate. float speed = UploadSettings.UPLOAD_SPEED.getValue(); // the total bandwith available then, is the percentage // allocated of the total bandwith. float totalBandwith = connectionSpeed * speed / 100.0f; return totalBandwith; } public int measuredUploadSpeed() { // Note that no lock is needed. return highestSpeed; } /** * Notes that some uploader has uploaded the given number of BYTES in the * given number of milliseconds. If bytes is too small, the data may be * ignored. * * @requires this' lock held * @modifies this.speed, this.speeds */ private void reportUploadSpeed(long milliseconds, long bytes) { // This is critical for ignoring 404's messages, etc. if (bytes < MIN_SAMPLE_BYTES) return; // Calculate the bandwidth in kiloBITS/s. We just assume that 1 kilobyte // is 1000 (not 1024) bytes for simplicity. int bandwidth = 8 * (int) ((float) bytes / (float) milliseconds); synchronized (speeds) { speeds.add(bandwidth); // Update maximum speed if possible if (speeds.size() >= MIN_SPEED_SAMPLE_SIZE) { int max = 0; for (int i = 0; i < speeds.size(); i++) max = Math.max(max, speeds.get(i)); this.highestSpeed = max; } } } public void measureBandwidth() { slotManager.measureBandwidth(); for (HTTPUploader active : localUploads) { active.measureBandwidth(); } } public float getMeasuredBandwidth() { float bw = 0; try { bw += slotManager.getMeasuredBandwidth(); } catch (InsufficientDataException notEnough) { } for (HTTPUploader forced : localUploads) { try { bw += forced.getMeasuredBandwidth(); } catch (InsufficientDataException e) { } } synchronized (this) { averageBandwidth = ((averageBandwidth * numMeasures) + bw) / ++numMeasures; } lastMeasuredBandwidth = bw; return bw; } public synchronized float getAverageBandwidth() { return averageBandwidth; } public float getLastMeasuredBandwidth() { return lastMeasuredBandwidth; } public UploadSlotManager getSlotManager() { return slotManager; } public HTTPUploadSession getOrCreateSession(HttpContext context) { assert started; HTTPUploadSession session = (HTTPUploadSession) context .getAttribute(SESSION_KEY); if (session == null) { HttpInetConnection conn = (HttpInetConnection) context .getAttribute(ExecutionContext.HTTP_CONNECTION); InetAddress host; if (conn != null) { host = conn.getRemoteAddress(); } else { // XXX this is a bad work around to make request processing // testable without having an underlying connection try { host = InetAddress.getLocalHost(); } catch (UnknownHostException e) { throw new RuntimeException(e); } } session = new HTTPUploadSession(getSlotManager(), host, HttpContextParams.getIOSession(context)); context.setAttribute(SESSION_KEY, session); } return session; } /** * Returns the session stored in <code>context</code>. * * @return null, if no session exists */ public HTTPUploadSession getSession(HttpContext context) { assert started; HTTPUploadSession session = (HTTPUploadSession) context .getAttribute(SESSION_KEY); return session; } public HTTPUploader getOrCreateUploader(HttpRequest request, HttpContext context, UploadType type, String filename) { return getOrCreateUploader(request, context, type, filename, null); } public HTTPUploader getOrCreateUploader(HttpRequest request, HttpContext context, UploadType type, String filename, String friendID) { assert started; HTTPUploadSession session = getOrCreateSession(context); HTTPUploader uploader = session.getUploader(); if (LOG.isDebugEnabled()) { LOG.debug("request line: " + request.getRequestLine()); LOG.debug("session: " + session); LOG.debug("uploader: " + uploader); } if (uploader != null) { if (!uploader.getFileName().equals(filename) || !uploader.getMethod().equals( request.getRequestLine().getMethod())) { if (LOG.isDebugEnabled()) { LOG.debug("uploader filename: " + uploader.getFileName() + " vs filename:" + filename); LOG.debug("uploader method: " + uploader.getMethod() + " vs method: " + request.getRequestLine().getMethod()); LOG.debug("request line: " + request.getRequestLine()); } // Because queuing is per-socket (and not per file), // we do not want to reset the queue status if they're // requesting a new file. if (session.isQueued()) { // However, we DO want to make sure that the old file // is interpreted as interrupted. Otherwise, // the GUI would show two lines with the the same slot // until the newer line finished, at which point // the first one would display as a -1 queue position. uploader.setState(UploadStatus.INTERRUPTED); } cleanupFinishedUploader(uploader); uploader = new HTTPUploader(filename, session, tcpBandwidthStatistics); uploader.setFriendId(friendID); } else { // reuse existing uploader object uploader.reinitialize(); } } else { // first request for this session uploader = new HTTPUploader(filename, session); uploader.setFriendId(friendID); } String method = request.getRequestLine().getMethod(); uploader.setMethod(method); uploader.setUploadType("HEAD".equals(method) ? UploadType.HEAD_REQUEST : type); session.setUploader(uploader); return uploader; } public HTTPUploader getUploader(HttpContext context) { assert started; HTTPUploadSession session = getSession(context); HTTPUploader uploader = session.getUploader(); assert uploader != null; return uploader; } public QueueStatus enqueue(HttpContext context, HttpRequest request) { assert started; HTTPUploadSession session = getSession(context); assert !session.isAccepted(); if (shouldBypassQueue(request, session.getUploader())) { session.setQueueStatus(QueueStatus.BYPASS); } else if (HttpContextParams.isLocal(context)) { session.setQueueStatus(QueueStatus.ACCEPTED); } else { session.setQueueStatus(checkAndQueue(session, context)); } if (LOG.isDebugEnabled()) LOG.debug("Queued upload: " + session); return session.getQueueStatus(); } /** For testing: removes all uploaders and clears the request cache. */ public void cleanup() { assert started; for (HTTPUploader uploader : activeUploadList .toArray(new HTTPUploader[0])) { uploader.stop(); slotManager.cancelRequest(uploader.getSession()); removeFromList(uploader); } slotManager.cleanup(); REQUESTS.clear(); } /** * Manages the {@link HTTPUploadSession} associated with a connection. */ private class ResponseListener implements HttpAcceptorListener { public void connectionOpen(NHttpConnection conn) { } public void connectionClosed(NHttpConnection conn) { assert started; HTTPUploadSession session = getSession(conn.getContext()); if (session != null) { HTTPUploader uploader = session.getUploader(); if (uploader != null) { if (LOG.isDebugEnabled()) LOG.debug("Closing session: " + session); boolean stillInQueue = slotManager.positionInQueue(session) > -1; slotManager.cancelRequest(session); if (stillInQueue) { // If it was queued, also set the state to INTERRUPTED // (changing from COMPLETE) uploader.setState(UploadStatus.INTERRUPTED); } else if (uploader.getState() != UploadStatus.COMPLETE) { // the complete state may have been set by // responseSent() already uploader.setState(UploadStatus.COMPLETE); } uploader.setLastResponse(null); cleanupFinishedUploader(uploader); session.setUploader(null); } } } public void responseSent(NHttpConnection conn, HttpResponse response) { assert started; HTTPUploadSession session = getSession(conn.getContext()); if (session != null) { HTTPUploader uploader = session.getUploader(); if (uploader != null && uploader.getLastResponse() == response) { uploader.setLastResponse(null); uploader.setState(UploadStatus.COMPLETE); } if (session.getQueueStatus() == QueueStatus.QUEUED) { session.getIOSession().setSocketTimeout( HTTPUploadSession.MAX_POLL_TIME); conn.requestInput(); // make sure the new socket timeout is used. } } } } }