示例#1
0
            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
                });
            }
示例#2
0
            private void FinishRequest(EasyRequest completedOperation, int messageResult)
            {
                if (completedOperation.ResponseMessage.StatusCode != HttpStatusCode.Unauthorized && completedOperation.Handler.PreAuthenticate)
                {
                    ulong availedAuth;
                    if (Interop.libcurl.curl_easy_getinfo(completedOperation.EasyHandle, CURLINFO.CURLINFO_HTTPAUTH_AVAIL, out availedAuth) == CURLcode.CURLE_OK)
                    {
                        // TODO: fix locking in AddCredentialToCache
                        completedOperation.Handler.AddCredentialToCache(
                            completedOperation.RequestMessage.RequestUri, availedAuth, completedOperation.NetworkCredential);
                    }
                    // Ignore errors: no need to fail for the sake of putting the credentials into the cache
                }

                switch (messageResult)
                {
                case CURLcode.CURLE_OK:
                    completedOperation.EnsureResponseMessagePublished();
                    break;

                default:
                    completedOperation.FailRequest(CreateHttpRequestException(new CurlException(messageResult, isMulti: false)));
                    break;
                }

                // At this point, we've completed processing the entire request, either due to error
                // or due to completing the entire response.
                completedOperation.Cleanup();
            }
示例#3
0
 private void ConfigureAndQueue(EasyRequest easy)
 {
     try
     {
         easy.InitializeCurl();
         _agent.Queue(new MultiAgent.IncomingRequest {
             Easy = easy, Type = MultiAgent.IncomingRequestType.New
         });
     }
     catch (Exception exc)
     {
         easy.FailRequest(exc);
         easy.Cleanup(); // no active processing remains, so we can cleanup
     }
 }
示例#4
0
            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.");
                }
            }
示例#5
0
        /// <summary>
        /// Loads the request's request content stream asynchronously and
        /// then submits the request to the multi agent.
        /// </summary>
        private async Task <HttpResponseMessage> QueueOperationWithRequestContentAsync(EasyRequest easy)
        {
            Debug.Assert(easy.RequestMessage.Content != null, "Expected request to have non-null request content");

            easy.RequestContentStream = await easy.RequestMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);

            if (easy.CancellationToken.IsCancellationRequested)
            {
                easy.FailRequest(new OperationCanceledException(easy.CancellationToken));
                easy.Cleanup(); // no active processing remains, so we can cleanup
            }
            else
            {
                _agent.Queue(easy);
            }
            return(await easy.Task.ConfigureAwait(false));
        }
            private void FinishRequest(EasyRequest completedOperation, CURLcode messageResult)
            {
                VerboseTrace("messageResult: " + messageResult, easy: completedOperation);

                if (completedOperation._responseMessage.StatusCode != HttpStatusCode.Unauthorized)
                {
                    if (completedOperation._handler.PreAuthenticate)
                    {
                        long authAvailable;
                        if (Interop.Http.EasyGetInfoLong(completedOperation._easyHandle, CURLINFO.CURLINFO_HTTPAUTH_AVAIL, out authAvailable) == CURLcode.CURLE_OK)
                        {
                            completedOperation._handler.AddCredentialToCache(
                                completedOperation._requestMessage.RequestUri, (CURLAUTH)authAvailable, completedOperation._networkCredential);
                        }
                        // Ignore errors: no need to fail for the sake of putting the credentials into the cache
                    }

                    completedOperation._handler.AddResponseCookies(
                        completedOperation._requestMessage.RequestUri, completedOperation._responseMessage);
                }

                // Complete or fail the request
                try
                {
                    bool unsupportedProtocolRedirect = messageResult == CURLcode.CURLE_UNSUPPORTED_PROTOCOL && completedOperation._isRedirect;
                    if (!unsupportedProtocolRedirect)
                    {
                        ThrowIfCURLEError(messageResult);
                    }
                    completedOperation.EnsureResponseMessagePublished();
                }
                catch (Exception exc)
                {
                    completedOperation.FailRequest(exc);
                }

                // At this point, we've completed processing the entire request, either due to error
                // or due to completing the entire response.
                completedOperation.Cleanup();
            }
            private void FinishRequest(EasyRequest completedOperation, int messageResult)
            {
                VerboseTrace("messageResult: " + messageResult, easy: completedOperation);

                if (completedOperation._responseMessage.StatusCode != HttpStatusCode.Unauthorized && completedOperation._handler.PreAuthenticate)
                {
                    ulong availedAuth;
                    if (Interop.libcurl.curl_easy_getinfo(completedOperation._easyHandle, CURLINFO.CURLINFO_HTTPAUTH_AVAIL, out availedAuth) == CURLcode.CURLE_OK)
                    {
                        // TODO: fix locking in AddCredentialToCache
                        completedOperation._handler.AddCredentialToCache(
                            completedOperation._requestMessage.RequestUri, availedAuth, completedOperation._networkCredential);
                    }
                    // Ignore errors: no need to fail for the sake of putting the credentials into the cache
                }

                switch (messageResult)
                {
                    case CURLcode.CURLE_OK:
                        completedOperation.EnsureResponseMessagePublished();
                        break;
                    default:
                        completedOperation.FailRequest(CreateHttpRequestException(new CurlException(messageResult, isMulti: false)));
                        break;
                }

                // At this point, we've completed processing the entire request, either due to error
                // or due to completing the entire response.
                completedOperation.Cleanup();
            }
            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 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 });
            }
示例#10
0
            private void FinishRequest(EasyRequest completedOperation, CURLcode messageResult)
            {
                VerboseTrace("messageResult: " + messageResult, easy: completedOperation);

                if (completedOperation._responseMessage.StatusCode != HttpStatusCode.Unauthorized)
                {
                    if (completedOperation._handler.PreAuthenticate)
                    {
                        long authAvailable;
                        if (Interop.Http.EasyGetInfoLong(completedOperation._easyHandle, CURLINFO.CURLINFO_HTTPAUTH_AVAIL, out authAvailable) == CURLcode.CURLE_OK)
                        {
                            completedOperation._handler.AddCredentialToCache(
                               completedOperation._requestMessage.RequestUri, (CURLAUTH)authAvailable, completedOperation._networkCredential);
                        }
                        // Ignore errors: no need to fail for the sake of putting the credentials into the cache
                    }

                    completedOperation._handler.AddResponseCookies(
                        completedOperation._requestMessage.RequestUri, completedOperation._responseMessage);
                }

                // Complete or fail the request
                try
                {
                    bool unsupportedProtocolRedirect = messageResult == CURLcode.CURLE_UNSUPPORTED_PROTOCOL && completedOperation._isRedirect;
                    if (!unsupportedProtocolRedirect)
                    {
                        ThrowIfCURLEError(messageResult);
                    }
                    completedOperation.EnsureResponseMessagePublished();
                }
                catch (Exception exc)
                {
                    completedOperation.FailRequest(exc);
                }

                // At this point, we've completed processing the entire request, either due to error
                // or due to completing the entire response.
                completedOperation.Cleanup();
            }
示例#11
0
        protected internal override Task <HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request", SR.net_http_handler_norequest);
            }

            if (request.RequestUri.Scheme == UriSchemeHttps)
            {
                if (!s_supportsSSL)
                {
                    throw new PlatformNotSupportedException(SR.net_http_unix_https_support_unavailable_libcurl);
                }
            }
            else
            {
                Debug.Assert(request.RequestUri.Scheme == UriSchemeHttp, "HttpClient expected to validate scheme as http or https.");
            }

            if (request.Headers.TransferEncodingChunked.GetValueOrDefault() && (request.Content == null))
            {
                throw new InvalidOperationException(SR.net_http_chunked_not_allowed_with_empty_content);
            }

            if (_useCookie && _cookieContainer == null)
            {
                throw new InvalidOperationException(SR.net_http_invalid_cookiecontainer);
            }

            CheckDisposed();
            SetOperationStarted();

            // Do an initial cancellation check to avoid initiating the async operation if
            // cancellation has already been requested.  After this, we'll rely on CancellationToken.Register
            // to notify us of cancellation requests and shut down the operation if possible.
            if (cancellationToken.IsCancellationRequested)
            {
                return(Task.FromCanceled <HttpResponseMessage>(cancellationToken));
            }

            // Create the easy request.  This associates the easy request with this handler and configures
            // it based on the settings configured for the handler.
            var easy = new EasyRequest(this, request, cancellationToken);

            try
            {
                easy.InitializeCurl();
                if (easy._requestContentStream != null)
                {
                    easy._requestContentStream.Run();
                }
                _agent.Queue(new MultiAgent.IncomingRequest {
                    Easy = easy, Type = MultiAgent.IncomingRequestType.New
                });
            }
            catch (Exception exc)
            {
                easy.FailRequest(exc);
                easy.Cleanup(); // no active processing remains, so we can cleanup
            }
            return(easy.Task);
        }
示例#12
0
        protected internal override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request", SR.net_http_handler_norequest);
            }

            if (request.RequestUri.Scheme == UriSchemeHttps)
            {
                if (!s_supportsSSL)
                {
                    throw new PlatformNotSupportedException(SR.net_http_unix_https_support_unavailable_libcurl);
                }
            }
            else
            {
                Debug.Assert(request.RequestUri.Scheme == UriSchemeHttp, "HttpClient expected to validate scheme as http or https.");
            }

            if (request.Headers.TransferEncodingChunked.GetValueOrDefault() && (request.Content == null))
            {
                throw new InvalidOperationException(SR.net_http_chunked_not_allowed_with_empty_content);
            }

            if (_useCookie && _cookieContainer == null)
            {
                throw new InvalidOperationException(SR.net_http_invalid_cookiecontainer);
            }

            CheckDisposed();
            SetOperationStarted();

            // Do an initial cancellation check to avoid initiating the async operation if
            // cancellation has already been requested.  After this, we'll rely on CancellationToken.Register
            // to notify us of cancellation requests and shut down the operation if possible.
            if (cancellationToken.IsCancellationRequested)
            {
                return Task.FromCanceled<HttpResponseMessage>(cancellationToken);
            }

            // Create the easy request.  This associates the easy request with this handler and configures
            // it based on the settings configured for the handler.
            var easy = new EasyRequest(this, request, cancellationToken);
            try
            {
                easy.InitializeCurl();
                if (easy._requestContentStream != null)
                {
                    easy._requestContentStream.Run();
                }
                _agent.Queue(new MultiAgent.IncomingRequest { Easy = easy, Type = MultiAgent.IncomingRequestType.New });
            }
            catch (Exception exc)
            {
                easy.FailRequest(exc);
                easy.Cleanup(); // no active processing remains, so we can cleanup
            }
            return easy.Task;
        }
示例#13
0
        /// <summary>
        /// Loads the request's request content stream asynchronously and 
        /// then submits the request to the multi agent.
        /// </summary>
        private async Task<HttpResponseMessage> QueueOperationWithRequestContentAsync(EasyRequest easy)
        {
            Debug.Assert(easy._requestMessage.Content != null, "Expected request to have non-null request content");

            easy._requestContentStream = await easy._requestMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);
            if (easy._cancellationToken.IsCancellationRequested)
            {
                easy.FailRequest(new OperationCanceledException(easy._cancellationToken));
                easy.Cleanup(); // no active processing remains, so we can cleanup
            }
            else
            {
                ConfigureAndQueue(easy);
            }
            return await easy.Task.ConfigureAwait(false);
        }
示例#14
0
 private void ConfigureAndQueue(EasyRequest easy)
 {
     try
     {
         easy.InitializeCurl();
         _agent.Queue(new MultiAgent.IncomingRequest { Easy = easy, Type = MultiAgent.IncomingRequestType.New });
     }
     catch (Exception exc)
     {
         easy.FailRequest(exc);
         easy.Cleanup(); // no active processing remains, so we can cleanup
     }
 }
示例#15
0
            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();
                }
            }