private void DeactivateActiveRequest( SafeCurlMultiHandle multiHandle, EasyRequest easy, IntPtr gcHandlePtr, CancellationTokenRegistration cancellationRegistration) { // Remove the operation from the multi handle so we can shut down the multi handle cleanly CURLMcode removeResult = Interop.Http.MultiRemoveHandle(multiHandle, easy._easyHandle); Debug.Assert(removeResult == CURLMcode.CURLM_OK, "Failed to remove easy handle"); // ignore cleanup errors in release // Release the associated GCHandle so that it's not kept alive forever if (gcHandlePtr != IntPtr.Zero) { try { GCHandle.FromIntPtr(gcHandlePtr).Free(); _activeOperations.Remove(gcHandlePtr); } catch (InvalidOperationException) { Debug.Fail("Couldn't get/free the GCHandle for an active operation while shutting down due to failure"); } } // Undo cancellation registration cancellationRegistration.Dispose(); }
private void ActivateNewRequest(SafeCurlMultiHandle multiHandle, EasyRequest easy) { Debug.Assert(easy != null, "We should never get a null request"); Debug.Assert(easy._associatedMultiAgent == null, "New requests should not be associated with an agent yet"); // If cancellation has been requested, complete the request proactively if (easy._cancellationToken.IsCancellationRequested) { easy.FailRequest(new OperationCanceledException(easy._cancellationToken)); easy.Cleanup(); // no active processing remains, so cleanup return; } // Otherwise, configure it. Most of the configuration was already done when the EasyRequest // was created, but there's additional configuration we need to do specific to this // multi agent, specifically telling the easy request about its own GCHandle and setting // up callbacks for data processing. Once it's configured, add it to the multi handle. GCHandle gcHandle = GCHandle.Alloc(easy); IntPtr gcHandlePtr = GCHandle.ToIntPtr(gcHandle); try { easy._associatedMultiAgent = this; easy.SetCurlOption(CURLoption.CURLOPT_PRIVATE, gcHandlePtr); easy.SetCurlCallbacks(gcHandlePtr, s_receiveHeadersCallback, s_sendCallback, s_seekCallback, s_receiveBodyCallback); ThrowIfCURLMError(Interop.Http.MultiAddHandle(multiHandle, easy._easyHandle)); } catch (Exception exc) { gcHandle.Free(); easy.FailRequest(exc); easy.Cleanup(); // no active processing remains, so cleanup return; } // And if cancellation can be requested, hook up a cancellation callback. // This callback will put the easy request back into the queue, which will // ensure that a wake-up request has been issued. When we pull // the easy request out of the request queue, we'll see that it's already // associated with this agent, meaning that it's a cancellation request, // and we'll deal with it appropriately. var cancellationReg = default(CancellationTokenRegistration); if (easy._cancellationToken.CanBeCanceled) { cancellationReg = easy._cancellationToken.Register(s => { var state = (Tuple<MultiAgent, EasyRequest>)s; state.Item1.Queue(new IncomingRequest { Easy = state.Item2, Type = IncomingRequestType.Cancel }); }, Tuple.Create<MultiAgent, EasyRequest>(this, easy)); } // Finally, add it to our map. _activeOperations.Add( gcHandlePtr, new ActiveRequest { Easy = easy, CancellationRegistration = cancellationReg }); }
private void FindAndFailActiveRequest(SafeCurlMultiHandle multiHandle, EasyRequest easy, Exception error) { VerboseTrace("Error: " + error.Message, easy: easy); IntPtr gcHandlePtr; ActiveRequest activeRequest; if (FindActiveRequest(easy, out gcHandlePtr, out activeRequest)) { DeactivateActiveRequest(multiHandle, easy, gcHandlePtr, activeRequest.CancellationRegistration); easy.FailRequest(error); easy.Cleanup(); // no active processing remains, so we can cleanup } else { Debug.Assert(easy.Task.IsCompleted, "We should only not be able to find the request if it failed or we started to send back the response."); } }
private void HandleIncomingRequest(SafeCurlMultiHandle multiHandle, IncomingRequest request) { Debug.Assert(!Monitor.IsEntered(_incomingRequests), "Incoming requests lock should only be held while accessing the queue"); VerboseTrace("Type: " + request.Type, easy: request.Easy); EasyRequest easy = request.Easy; switch (request.Type) { case IncomingRequestType.New: ActivateNewRequest(multiHandle, easy); break; case IncomingRequestType.Cancel: Debug.Assert(easy._associatedMultiAgent == this, "Should only cancel associated easy requests"); Debug.Assert(easy._cancellationToken.IsCancellationRequested, "Cancellation should have been requested"); FindAndFailActiveRequest(multiHandle, easy, new OperationCanceledException(easy._cancellationToken)); break; case IncomingRequestType.Unpause: Debug.Assert(easy._associatedMultiAgent == this, "Should only unpause associated easy requests"); if (!easy._easyHandle.IsClosed) { IntPtr gcHandlePtr; ActiveRequest ar; Debug.Assert(FindActiveRequest(easy, out gcHandlePtr, out ar), "Couldn't find active request for unpause"); CURLcode unpauseResult = Interop.Http.EasyUnpause(easy._easyHandle); try { ThrowIfCURLEError(unpauseResult); } catch (Exception exc) { FindAndFailActiveRequest(multiHandle, easy, exc); } } break; default: Debug.Fail("Invalid request type: " + request.Type); break; } }
/// <summary>Creates and configures a new multi handle.</summary> private SafeCurlMultiHandle CreateAndConfigureMultiHandle() { // Create the new handle SafeCurlMultiHandle multiHandle = Interop.Http.MultiCreate(); if (multiHandle.IsInvalid) { throw CreateHttpRequestException(); } // In support of HTTP/2, enable HTTP/2 connections to be multiplexed if possible. // We must only do this if the version of libcurl being used supports HTTP/2 multiplexing. // Due to a change in a libcurl signature, if we try to make this call on an older libcurl, // we'll end up accidentally and unconditionally enabling HTTP 1.1 pipelining. if (s_supportsHttp2Multiplexing) { ThrowIfCURLMError(Interop.Http.MultiSetOptionLong(multiHandle, Interop.Http.CURLMoption.CURLMOPT_PIPELINING, (long)Interop.Http.CurlPipe.CURLPIPE_MULTIPLEX)); } return multiHandle; }
private void HandleIncomingRequest(SafeCurlMultiHandle multiHandle, IncomingRequest request) { Debug.Assert(!Monitor.IsEntered(_incomingRequests), "Incoming requests lock should only be held while accessing the queue"); VerboseTrace("Type: " + request.Type, easy: request.Easy); EasyRequest easy = request.Easy; switch (request.Type) { case IncomingRequestType.New: ActivateNewRequest(multiHandle, easy); break; case IncomingRequestType.Cancel: Debug.Assert(easy._associatedMultiAgent == this, "Should only cancel associated easy requests"); Debug.Assert(easy._cancellationToken.IsCancellationRequested, "Cancellation should have been requested"); FindAndFailActiveRequest(multiHandle, easy, new OperationCanceledException(easy._cancellationToken)); break; case IncomingRequestType.Unpause: Debug.Assert(easy._associatedMultiAgent == this, "Should only unpause associated easy requests"); if (!easy._easyHandle.IsClosed) { IntPtr gcHandlePtr; ActiveRequest ar; Debug.Assert(FindActiveRequest(easy, out gcHandlePtr, out ar), "Couldn't find active request for unpause"); CURLcode unpauseResult = Interop.Http.EasyUnpause(easy._easyHandle); try { ThrowIfCURLEError(unpauseResult); } catch (Exception exc) { FindAndFailActiveRequest(multiHandle, easy, exc); } } break; default: Debug.Fail("Invalid request type: " + request.Type); break; } }
private void WorkerLoop() { Debug.Assert(!Monitor.IsEntered(_incomingRequests), "No locks should be held while invoking Process"); Debug.Assert(_runningWorker != null && _runningWorker.Id == Task.CurrentId, "This is the worker, so it must be running"); Debug.Assert(_wakeupRequestedPipeFd != null && !_wakeupRequestedPipeFd.IsInvalid, "Should have a valid pipe for wake ups"); // Create the multi handle to use for this round of processing. This one handle will be used // to service all easy requests currently available and all those that come in while // we're processing other requests. Once the work quiesces and there are no more requests // to process, this multi handle will be released as the worker goes away. The next // time a request arrives and a new worker is spun up, a new multi handle will be created. SafeCurlMultiHandle multiHandle = CreateAndConfigureMultiHandle(); // Clear our active operations table. This should already be clear, either because // all previous operations completed without unexpected exception, or in the case of an // unexpected exception we should have cleaned up gracefully anyway. But just in case... Debug.Assert(_activeOperations.Count == 0, "We shouldn't have any active operations when starting processing."); _activeOperations.Clear(); bool endingSuccessfully = false; try { // Continue processing as long as there are any active operations while (true) { // First handle any requests in the incoming requests queue. while (true) { IncomingRequest request; lock (_incomingRequests) { if (_incomingRequests.Count == 0) break; request = _incomingRequests.Dequeue(); } HandleIncomingRequest(multiHandle, request); } // If we have no active operations, we're done. if (_activeOperations.Count == 0) { endingSuccessfully = true; return; } // We have one or more active operations. Run any work that needs to be run. ThrowIfCURLMError(Interop.Http.MultiPerform(multiHandle)); // Complete and remove any requests that have finished being processed. CURLMSG message; IntPtr easyHandle; CURLcode result; while (Interop.Http.MultiInfoRead(multiHandle, out message, out easyHandle, out result)) { Debug.Assert(message == CURLMSG.CURLMSG_DONE, "CURLMSG_DONE is supposed to be the only message type"); if (message == CURLMSG.CURLMSG_DONE) { IntPtr gcHandlePtr; CURLcode getInfoResult = Interop.Http.EasyGetInfoPointer(easyHandle, CURLINFO.CURLINFO_PRIVATE, out gcHandlePtr); Debug.Assert(getInfoResult == CURLcode.CURLE_OK, "Failed to get info on a completing easy handle"); if (getInfoResult == CURLcode.CURLE_OK) { ActiveRequest completedOperation; bool gotActiveOp = _activeOperations.TryGetValue(gcHandlePtr, out completedOperation); Debug.Assert(gotActiveOp, "Expected to find GCHandle ptr in active operations table"); if (gotActiveOp) { DeactivateActiveRequest(multiHandle, completedOperation.Easy, gcHandlePtr, completedOperation.CancellationRegistration); FinishRequest(completedOperation.Easy, result); } } } } // Wait for more things to do. bool isWakeupRequestedPipeActive; bool isTimeout; ThrowIfCURLMError(Interop.Http.MultiWait(multiHandle, _wakeupRequestedPipeFd, out isWakeupRequestedPipeActive, out isTimeout)); if (isWakeupRequestedPipeActive) { // We woke up (at least in part) because a wake-up was requested. // Read the data out of the pipe to clear it. Debug.Assert(!isTimeout, "should not have timed out if isExtraFileDescriptorActive"); VerboseTrace("curl_multi_wait wake-up notification"); ReadFromWakeupPipeWhenKnownToContainData(); } VerboseTraceIf(isTimeout, "curl_multi_wait timeout"); // PERF NOTE: curl_multi_wait uses poll (assuming it's available), which is O(N) in terms of the number of fds // being waited on. If this ends up being a scalability bottleneck, we can look into using the curl_multi_socket_* // APIs, which would let us switch to using epoll by being notified when sockets file descriptors are added or // removed and configuring the epoll context with EPOLL_CTL_ADD/DEL, which at the expense of a lot of additional // complexity would let us turn the O(N) operation into an O(1) operation. The additional complexity would come // not only in the form of additional callbacks and managing the socket collection, but also in the form of timer // management, which is necessary when using the curl_multi_socket_* APIs and which we avoid by using just // curl_multi_wait/perform. } } finally { // If we got an unexpected exception, something very bad happened. We may have some // operations that we initiated but that weren't completed. Make sure to clean up any // such operations, failing them and releasing their resources. if (_activeOperations.Count > 0) { Debug.Assert(!endingSuccessfully, "We should only have remaining operations if we got an unexpected exception"); foreach (KeyValuePair<IntPtr, ActiveRequest> pair in _activeOperations) { ActiveRequest failingOperation = pair.Value; IntPtr failingOperationGcHandle = pair.Key; DeactivateActiveRequest(multiHandle, failingOperation.Easy, failingOperationGcHandle, failingOperation.CancellationRegistration); // Complete the operation's task and clean up any of its resources failingOperation.Easy.FailRequest(CreateHttpRequestException()); failingOperation.Easy.Cleanup(); // no active processing remains, so cleanup } // Clear the table. _activeOperations.Clear(); } // Finally, dispose of the multi handle. multiHandle.Dispose(); } }