diff --git a/src/Common/src/Interop/Unix/System.Net.Http.Native/Interop.VersionInfo.cs b/src/Common/src/Interop/Unix/System.Net.Http.Native/Interop.VersionInfo.cs index 09a60043557b..1899fd0af3ca 100644 --- a/src/Common/src/Interop/Unix/System.Net.Http.Native/Interop.VersionInfo.cs +++ b/src/Common/src/Interop/Unix/System.Net.Http.Native/Interop.VersionInfo.cs @@ -49,5 +49,6 @@ internal enum CurlFeatures : int internal const string OpenSsl10Description = "openssl/1.0"; internal const string SecureTransportDescription = "SecureTransport"; + internal const string LibreSslDescription = "LibreSSL"; } } diff --git a/src/System.Net.Http/src/System/Net/Http/OSX/CurlHandler.SslProvider.cs b/src/System.Net.Http/src/System/Net/Http/OSX/CurlHandler.SslProvider.cs index 4c84760379d0..ba11f8433da3 100644 --- a/src/System.Net.Http/src/System/Net/Http/OSX/CurlHandler.SslProvider.cs +++ b/src/System.Net.Http/src/System/Net/Http/OSX/CurlHandler.SslProvider.cs @@ -12,6 +12,19 @@ namespace System.Net.Http { internal partial class CurlHandler : HttpMessageHandler { + static partial void UseSingletonMultiAgent(ref bool result) + { + // Some backends other than OpenSSL need locks initialized in order to use them in a + // multithreaded context, which would happen with multiple HttpClients and thus multiple + // MultiAgents. Since we don't currently have the ability to do so initialization, instead we + // restrict all HttpClients to use the same MultiAgent instance in this case. We know LibreSSL + // is in this camp, so we currently special-case it. + string curlSslVersion = Interop.Http.GetSslVersionDescription(); + result = + !string.IsNullOrEmpty(curlSslVersion) && + curlSslVersion.StartsWith(Interop.Http.LibreSslDescription, StringComparison.OrdinalIgnoreCase); + } + private static class SslProvider { internal static void SetSslOptions(EasyRequest easy, ClientCertificateOption clientCertOption) diff --git a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.MultiAgent.cs b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.MultiAgent.cs index 67473615bcb9..209ccc5ad03d 100644 --- a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.MultiAgent.cs +++ b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.MultiAgent.cs @@ -37,8 +37,8 @@ private sealed class MultiAgent : IDisposable private static readonly Interop.Http.ReadWriteCallback s_receiveBodyCallback = CurlReceiveBodyCallback; private static readonly Interop.Http.DebugCallback s_debugCallback = CurlDebugFunction; - /// CurlHandler that owns this MultiAgent. - private readonly CurlHandler _associatedHandler; + /// CurlHandler that created this MultiAgent. If null, this is a shared handler. + private readonly CurlHandler _creatingHandler; /// /// A collection of not-yet-processed incoming requests for work to be done @@ -79,18 +79,21 @@ private sealed class MultiAgent : IDisposable /// private Interop.Http.SafeCurlMultiHandle _multiHandle; + /// Set when Dispose has been called. + private bool _disposed; + /// Initializes the MultiAgent. - /// The handler that owns this agent. + /// The handler that created this agent, or null if it's shared. public MultiAgent(CurlHandler handler) { - Debug.Assert(handler != null, "Expected non-null handler"); - _associatedHandler = handler; + _creatingHandler = handler; } /// Disposes of the agent. public void Dispose() { EventSourceTrace(null); + _disposed = true; QueueIfRunning(new IncomingRequest { Type = IncomingRequestType.Shutdown }); _multiHandle?.Dispose(); } @@ -247,20 +250,25 @@ private Interop.Http.SafeCurlMultiHandle CreateAndConfigureMultiHandle() EventSourceTrace("Set multiplexing on multi handle"); } - // Configure max connections per host if it was changed from the default - int maxConnections = _associatedHandler.MaxConnectionsPerServer; - if (maxConnections < int.MaxValue) // int.MaxValue considered infinite, mapping to libcurl default of 0 + // Configure max connections per host if it was changed from the default. In shared mode, + // this will be pulled from the handler that first created the agent; the setting from subsequent + // handlers that use this will be ignored. + if (_creatingHandler != null) { - CURLMcode code = Interop.Http.MultiSetOptionLong(multiHandle, Interop.Http.CURLMoption.CURLMOPT_MAX_HOST_CONNECTIONS, maxConnections); - switch (code) + int maxConnections = _creatingHandler.MaxConnectionsPerServer; + if (maxConnections < int.MaxValue) // int.MaxValue considered infinite, mapping to libcurl default of 0 { - case CURLMcode.CURLM_OK: - EventSourceTrace("Set max host connections to {0}", maxConnections); - break; - default: - // Treat failures as non-fatal in release; worst case is we employ more connections than desired. - EventSourceTrace("Setting CURLMOPT_MAX_HOST_CONNECTIONS failed: {0}. Ignoring option.", code); - break; + CURLMcode code = Interop.Http.MultiSetOptionLong(multiHandle, Interop.Http.CURLMoption.CURLMOPT_MAX_HOST_CONNECTIONS, maxConnections); + switch (code) + { + case CURLMcode.CURLM_OK: + EventSourceTrace("Set max host connections to {0}", maxConnections); + break; + default: + // Treat failures as non-fatal in release; worst case is we employ more connections than desired. + EventSourceTrace("Setting CURLMOPT_MAX_HOST_CONNECTIONS failed: {0}. Ignoring option.", code); + break; + } } } @@ -306,7 +314,7 @@ private void WorkerBody() // more requests could have been added. If they were, // kick off another processing loop. _runningWorker = null; - if (_incomingRequests.Count > 0 && !_associatedHandler._disposed) + if (_incomingRequests.Count > 0 && !_disposed) { EnsureWorkerIsRunning(); } diff --git a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.cs b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.cs index 486231efeb48..6861c9df443e 100644 --- a/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.cs +++ b/src/System.Net.Http/src/System/Net/Http/Unix/CurlHandler.cs @@ -126,6 +126,7 @@ internal partial class CurlHandler : HttpMessageHandler private static string s_curlVersionDescription; private static string s_curlSslVersionDescription; + private static readonly MultiAgent s_singletonSharedAgent; private readonly MultiAgent _agent; private volatile bool _anyOperationStarted; private volatile bool _disposed; @@ -169,13 +170,28 @@ static CurlHandler() { EventSourceTrace($"libcurl: {CurlVersionDescription} {CurlSslVersionDescription} {features}"); } + + // By default every CurlHandler gets its own MultiAgent. But for some backends, + // we need to restrict the number of threads involved in processing libcurl work, + // so we create a single MultiAgent that's used by all handlers. + bool useSingleton = false; + UseSingletonMultiAgent(ref useSingleton); + if (useSingleton) + { + s_singletonSharedAgent = new MultiAgent(null); + } } public CurlHandler() { - _agent = new MultiAgent(this); + // If the shared MultiAgent was initialized, use it. + // Otherwise, create a new MultiAgent for this handler. + _agent = s_singletonSharedAgent ?? new MultiAgent(this); } + /// Overridden by another partial implementation to set to true if a single MultiAgent should be used. + static partial void UseSingletonMultiAgent(ref bool result); + #region Properties private static string CurlVersionDescription => s_curlVersionDescription ?? (s_curlVersionDescription = Interop.Http.GetVersionDescription() ?? string.Empty); @@ -414,7 +430,7 @@ internal bool UseDefaultCredentials protected override void Dispose(bool disposing) { _disposed = true; - if (disposing) + if (disposing && _agent != s_singletonSharedAgent) { _agent.Dispose(); }