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 }); }
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(); }
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 } }
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."); } }
/// <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 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 }); }
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); }
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; }
/// <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); }
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(); } }