private static void EndRequest(SafeCurlMultiHandle multiHandle, IntPtr statePtr, int result) { GCHandle stateHandle = GCHandle.FromIntPtr(statePtr); RequestCompletionSource state = (RequestCompletionSource)stateHandle.Target; try { // No more callbacks so no more data state.ResponseMessage.ContentStream.SignalComplete(); if (CURLcode.CURLE_OK == result) { state.TrySetResult(state.ResponseMessage); } else { state.TrySetException(new HttpRequestException(SR.net_http_client_execution_error, GetCurlException(result))); } } catch (Exception ex) { HandleAsyncException(state, ex); } finally { RemoveEasyHandle(multiHandle, stateHandle, true); } }
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 int removeResult = Interop.libcurl.curl_multi_remove_handle(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 static void RemoveEasyHandle(SafeCurlMultiHandle multiHandle, GCHandle stateHandle, bool onMultiStack) { RequestCompletionSource state = (RequestCompletionSource)stateHandle.Target; SafeCurlHandle requestHandle = state.RequestHandle; if (onMultiStack) { lock (multiHandle) { Interop.libcurl.curl_multi_remove_handle(multiHandle, requestHandle); } state.SessionHandle = null; requestHandle.DangerousRelease(); } if (!state.RequestHeaderHandle.IsInvalid) { SafeCurlSlistHandle headerHandle = state.RequestHeaderHandle; SafeCurlSlistHandle.DisposeAndClearHandle(ref headerHandle); } SafeCurlHandle.DisposeAndClearHandle(ref requestHandle); stateHandle.Free(); }
private void ActivateNewRequest(SafeCurlMultiHandle multiHandle, EasyRequest easy) { Debug.Assert(easy != null, "We should never get a null request"); // 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 { Debug.Assert(easy._associatedMultiAgent == null, "A request should only ever be associated with a single agent"); easy._associatedMultiAgent = this; easy.SetCurlOption(CURLoption.CURLOPT_PRIVATE, gcHandlePtr); SetCurlCallbacks(easy, gcHandlePtr); ThrowIfCURLMError(Interop.libcurl.curl_multi_add_handle(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 and then // 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(state.Item2); state.Item1.RequestWakeup(); }, Tuple.Create <MultiAgent, EasyRequest>(this, easy)); } // Finally, add it to our map. _activeOperations.Add( gcHandlePtr, new ActiveRequest { Easy = easy, CancellationRegistration = cancellationReg }); }
public static void DisposeAndClearHandle(ref SafeCurlMultiHandle curlHandle) { if (curlHandle != null) { curlHandle.Dispose(); curlHandle = null; } }
internal CurlHandler() { _multiHandle = Interop.libcurl.curl_multi_init(); if (_multiHandle.IsInvalid) { throw new HttpRequestException(SR.net_http_client_execution_error); } SetCurlMultiOptions(); }
internal CurlHandler() { _multiHandle = Interop.libcurl.curl_multi_init(); if (_multiHandle.IsInvalid) { throw new HttpRequestException(SR.net_http_client_execution_error); } _multiHandle.Timer = new Timer(CurlTimerElapsed, _multiHandle, Timeout.Infinite, Timeout.Infinite); SetCurlMultiOptions(); }
protected override void Dispose(bool disposing) { if (disposing && !_disposed) { _disposed = true; if (_multiHandlePtr.IsAllocated) { _multiHandlePtr.Free(); } _multiHandle = null; } base.Dispose(disposing); }
protected override void Dispose(bool disposing) { if (disposing && !_disposed) { _disposed = true; // TODO: Check for the correct place to free this since // its lifetime should match _multiHandle //if (_multiHandlePtr.IsAllocated) //{ // _multiHandlePtr.Free(); //} _multiHandle = null; } base.Dispose(disposing); }
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"); int unpauseResult = Interop.libcurl.curl_easy_pause(easy._easyHandle, Interop.libcurl.CURLPAUSE_CONT); try { ThrowIfCURLEError(unpauseResult); } catch (Exception exc) { FindAndFailActiveRequest(multiHandle, easy, exc); } } break; default: Debug.Fail("Invalid request type: " + request.Type); break; } }
private static void EndRequest(SafeCurlMultiHandle multiHandle, IntPtr statePtr, int result) { GCHandle stateHandle = GCHandle.FromIntPtr(statePtr); RequestCompletionSource state = (RequestCompletionSource)stateHandle.Target; try { // No more callbacks so no more data state.ResponseMessage.ContentStream.SignalComplete(); if (CURLcode.CURLE_OK == result) { state.TrySetResult(state.ResponseMessage); } else { state.TrySetException(new HttpRequestException(SR.net_http_client_execution_error, GetCurlException(result))); } if (state.ResponseMessage.StatusCode != HttpStatusCode.Unauthorized && state.Handler.PreAuthenticate) { ulong availedAuth; if (Interop.libcurl.curl_easy_getinfo(state.RequestHandle, CURLINFO.CURLINFO_HTTPAUTH_AVAIL, out availedAuth) == CURLcode.CURLE_OK) { state.Handler.AddCredentialToCache(state.RequestMessage.RequestUri, availedAuth, state.NetworkCredential); } // ignoring the exception in this case. // There is no point in killing the request for the sake of putting the credentials into the cache } } catch (Exception ex) { HandleAsyncException(state, ex); } finally { RemoveEasyHandle(multiHandle, stateHandle, true); } }
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 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 int removeResult = Interop.libcurl.curl_multi_remove_handle(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); SetCurlCallbacks(easy, gcHandlePtr); ThrowIfCURLMError(Interop.libcurl.curl_multi_add_handle(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 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"); Debug.Assert(easy._paused == EasyRequest.PauseState.UnpauseRequestIssued, "Unpause requests only make sense when a request has been issued"); easy._paused = EasyRequest.PauseState.Unpaused; int unpauseResult = Interop.libcurl.curl_easy_pause(easy._easyHandle, Interop.libcurl.CURLPAUSE_CONT); if (unpauseResult != CURLcode.CURLE_OK) { FindAndFailActiveRequest(multiHandle, easy, new CurlException(unpauseResult, isMulti: false)); } } break; default: Debug.Fail("Invalid request type: " + request.Type); break; } }
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 static void PollFunction(SafeCurlMultiHandle multiHandle) { List<Interop.libc.pollfd> fds = new List<Interop.libc.pollfd>(); while (true) { lock (multiHandle) { if (0 == multiHandle.RequestCount) { multiHandle.PollCancelled = true; return; } } multiHandle.PollFds(fds); if (0 == fds.Count) { // No read/write activity on the sockets.. keep polling continue; } int result = -1; for (int i = 0; i < fds.Count; i++) { int eventBitMask = ((fds[i].revents & PollFlags.POLLIN) != 0) ? CurlSelect.CURL_CSELECT_IN : 0; eventBitMask |= ((fds[i].revents & PollFlags.POLLOUT) != 0) ? CurlSelect.CURL_CSELECT_OUT : 0; if (eventBitMask != 0) { lock (multiHandle) { int runningTransfers; if (CURLMcode.CURLM_OK == Interop.libcurl.curl_multi_socket_action(multiHandle, fds[i].fd, eventBitMask, out runningTransfers)) { result = CURLMcode.CURLM_OK; if (0 == runningTransfers) { multiHandle.Timer.Change(Timeout.Infinite, Timeout.Infinite); } } // Ignore errors } } } if (CURLMcode.CURLM_OK == result) { CheckForCompletedTransfers(multiHandle); } } }
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 != 0, "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 = Interop.libcurl.curl_multi_init(); if (multiHandle.IsInvalid) { throw CreateHttpRequestException(); } // 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. int running_handles; ThrowIfCURLMError(Interop.libcurl.curl_multi_perform(multiHandle, out running_handles)); // Complete and remove any requests that have finished being processed. int pendingMessages; IntPtr messagePtr; while ((messagePtr = Interop.libcurl.curl_multi_info_read(multiHandle, out pendingMessages)) != IntPtr.Zero) { Interop.libcurl.CURLMsg message = Marshal.PtrToStructure <Interop.libcurl.CURLMsg>(messagePtr); Debug.Assert(message.msg == Interop.libcurl.CURLMSG.CURLMSG_DONE, "CURLMSG_DONE is supposed to be the only message type"); IntPtr gcHandlePtr; ActiveRequest completedOperation; if (message.msg == Interop.libcurl.CURLMSG.CURLMSG_DONE) { int getInfoResult = Interop.libcurl.curl_easy_getinfo(message.easy_handle, 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) { 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, message.result); } } } } // Wait for more things to do. Even with our cancellation mechanism, we specify a timeout so that // just in case something goes wrong we can recover gracefully. This timeout is relatively long. // Note, though, that libcurl has its own internal timeout, which can be requested separately // via curl_multi_timeout, but which is used implicitly by curl_multi_wait if it's shorter // than the value we provide. const int FailsafeTimeoutMilliseconds = 1000; int numFds; unsafe { Interop.libcurl.curl_waitfd extraFds = new Interop.libcurl.curl_waitfd { fd = _wakeupRequestedPipeFd, events = Interop.libcurl.CURL_WAIT_POLLIN, revents = 0 }; ThrowIfCURLMError(Interop.libcurl.curl_multi_wait(multiHandle, &extraFds, 1, FailsafeTimeoutMilliseconds, out numFds)); if ((extraFds.revents & Interop.libcurl.CURL_WAIT_POLLIN) != 0) { // 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(numFds >= 1, "numFds should have been at least one, as the extraFd was set"); VerboseTrace("curl_multi_wait wake-up notification"); ReadFromWakeupPipeWhenKnownToContainData(); } VerboseTraceIf(numFds == 0, "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(); } }
private static void CheckForCompletedTransfers(SafeCurlMultiHandle multiHandle) { int pendingMessages; IntPtr messagePtr; lock (multiHandle) { messagePtr = Interop.libcurl.curl_multi_info_read(multiHandle, out pendingMessages); } while (IntPtr.Zero != messagePtr) { var message = Marshal.PtrToStructure<Interop.libcurl.CURLMsg>(messagePtr); if (Interop.libcurl.CURLMSG.CURLMSG_DONE == message.msg) { IntPtr statePtr; int result = Interop.libcurl.curl_easy_getinfo(message.easy_handle, CURLINFO.CURLINFO_PRIVATE, out statePtr); if (result == CURLcode.CURLE_OK) { EndRequest(multiHandle, statePtr, message.result); } } lock (multiHandle) { messagePtr = Interop.libcurl.curl_multi_info_read(multiHandle, out pendingMessages); } } }
private static void CheckForCompletedTransfers(SafeCurlMultiHandle multiHandle, int socketFd, int eventBitMask, IntPtr statePtr) { int runningTransfers; // TODO: Revisit the lock and see if serialization is really needed lock (multiHandle) { int result = Interop.libcurl.curl_multi_socket_action(multiHandle, socketFd, eventBitMask, out runningTransfers); if (result != CURLMcode.CURLM_OK) { throw new HttpRequestException(SR.net_http_client_execution_error, GetCurlException(result, true)); } } // Let socket callback handle fd-specific message if (IntPtr.Zero == statePtr) { return; } int pendingMessages; IntPtr messagePtr; lock (multiHandle) { messagePtr = Interop.libcurl.curl_multi_info_read(multiHandle, out pendingMessages); } while (IntPtr.Zero != messagePtr) { var message = Marshal.PtrToStructure<Interop.libcurl.CURLMsg>(messagePtr); if (Interop.libcurl.CURLMSG.CURLMSG_DONE == message.msg) { EndRequest(multiHandle, statePtr, message.result); } lock (multiHandle) { messagePtr = Interop.libcurl.curl_multi_info_read(multiHandle, out pendingMessages); } } }
private void Process() { Debug.Assert(!Monitor.IsEntered(_incomingRequests), "No locks should be held while invoking Process"); Debug.Assert(_workerRunning, "This is the worker, so it must be running"); Debug.Assert(_wakeupRequestedPipeFd != 0, "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 = Interop.libcurl.curl_multi_init(); if (multiHandle.IsInvalid) { throw CreateHttpRequestException(); } // 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) { // Activate any new operations that were submitted, and cancel any operations // that should no longer be around. lock (_incomingRequests) { while (_incomingRequests.Count > 0) { EasyRequest easy = _incomingRequests.Dequeue(); Debug.Assert(easy._associatedMultiAgent == null || easy._associatedMultiAgent == this, "An incoming request must only be associated with no or this agent"); if (easy._associatedMultiAgent == null) { // Handle new request ActivateNewRequest(multiHandle, easy); } else { // Handle cancellation request. Debug.Assert(easy.CancellationToken.IsCancellationRequested, "_associatedMultiAgent should only be non-null if cancellation was requested"); IntPtr gcHandlePtr; ActiveRequest activeRequest; if (FindActiveRequest(easy, out gcHandlePtr, out activeRequest)) { DeactivateActiveRequest(multiHandle, easy, gcHandlePtr, activeRequest.CancellationRegistration); easy.FailRequest(new OperationCanceledException(easy.CancellationToken)); 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 was already completed."); } } } } // If we have no active operations, we're done. if (_activeOperations.Count == 0) { endingSuccessfully = true; return; } // We have one or more active operaitons. Run any work that needs to be run. int running_handles; ThrowIfCURLMError(Interop.libcurl.curl_multi_perform(multiHandle, out running_handles)); // Complete and remove any requests that have finished being processed. int pendingMessages; IntPtr messagePtr; while ((messagePtr = Interop.libcurl.curl_multi_info_read(multiHandle, out pendingMessages)) != IntPtr.Zero) { Interop.libcurl.CURLMsg message = Marshal.PtrToStructure <Interop.libcurl.CURLMsg>(messagePtr); IntPtr gcHandlePtr; ActiveRequest completedOperation; if (message.msg == Interop.libcurl.CURLMSG.CURLMSG_DONE && Interop.libcurl.curl_easy_getinfo(message.easy_handle, CURLINFO.CURLINFO_PRIVATE, out gcHandlePtr) == CURLcode.CURLE_OK && _activeOperations.TryGetValue(gcHandlePtr, out completedOperation)) { DeactivateActiveRequest(multiHandle, completedOperation.Easy, gcHandlePtr, completedOperation.CancellationRegistration); FinishRequest(completedOperation.Easy, message.result); } } // Wait for more things to do. Even with our cancellation mechanism, we specify a timeout so that // just in case something goes wrong we can recover gracefully. This timeout is relatively long. // Note, though, that libcurl has its own internal timeout, which can be requested separately // via curl_multi_timeout, but which is used implicitly by curl_multi_wait if it's shorter // than the value we provide. const int FailsafeTimeoutMilliseconds = 1000; int numFds; unsafe { Interop.libcurl.curl_waitfd extraFds = new Interop.libcurl.curl_waitfd { fd = _wakeupRequestedPipeFd, events = Interop.libcurl.CURL_WAIT_POLLIN, revents = 0 }; ThrowIfCURLMError(Interop.libcurl.curl_multi_wait(multiHandle, &extraFds, 1, FailsafeTimeoutMilliseconds, out numFds)); if ((extraFds.revents & Interop.libcurl.CURL_WAIT_POLLIN) != 0) { // We woke up (at least in part) because a wake-up was requested. Read the data out of the pipe // to clear it. It's possible but unlikely that there will be tons of extra data in the pipe, // more than we end up reading out here (it's unlikely because we only write a byte to the pipe when // transitioning from 0 to 1 incoming request or when cancellation is requested, and that would // need to happen many times in a single iteration). In that unlikely case, we'll simply loop // around again as normal and end up waking up spuriously from the next curl_multi_wait. For now, // this is preferable to making additional syscalls to poll and read from the pipe). const int ClearBufferSize = 4096; // some sufficiently large size to clear the pipe in any normal case byte * clearBuf = stackalloc byte[ClearBufferSize]; while (Interop.CheckIo((long)Interop.libc.read(_wakeupRequestedPipeFd, clearBuf, (IntPtr)ClearBufferSize))) { ; } } } // 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(); } }