diff --git a/sdk/core/Azure.Core/CHANGELOG.md b/sdk/core/Azure.Core/CHANGELOG.md index fd08ba68a4f8..36bf3e69427d 100644 --- a/sdk/core/Azure.Core/CHANGELOG.md +++ b/sdk/core/Azure.Core/CHANGELOG.md @@ -2,6 +2,9 @@ ## 1.6.0-beta.1 (Unreleased) +### Changed +- `ServicePointManager` Connection limit is automatically increased to `50` for Azure endpoints. + ## 1.5.0 (2020-09-03) diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs index de841a5fdf9a..365d73cd30a9 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs @@ -86,6 +86,10 @@ private static HttpClient CreateDefaultClient() httpClientHandler.Proxy = webProxy; } +#if NETFRAMEWORK + ServicePointHelpers.SetLimits(httpClientHandler); +#endif + return new HttpClient(httpClientHandler); } diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs index ef8201127a82..1187a2c71101 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs @@ -48,6 +48,9 @@ public override async ValueTask ProcessAsync(HttpMessage message) private async ValueTask ProcessInternal(HttpMessage message, bool async) { var request = CreateRequest(message.Request); + + ServicePointHelpers.SetLimits(request.ServicePoint); + using var registration = message.CancellationToken.Register(state => ((HttpWebRequest) state).Abort(), request); try { diff --git a/sdk/core/Azure.Core/src/Pipeline/ServicePointHelpers.cs b/sdk/core/Azure.Core/src/Pipeline/ServicePointHelpers.cs new file mode 100644 index 000000000000..ceb00987d5af --- /dev/null +++ b/sdk/core/Azure.Core/src/Pipeline/ServicePointHelpers.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Net; +using System.Net.Http; + +namespace Azure.Core.Pipeline +{ + internal static class ServicePointHelpers + { + private const int RuntimeDefaultConnectionLimit = 2; + private const int IncreasedConnectionLimit = 50; + + public static void SetLimits(ServicePoint requestServicePoint) + { + // Only change when the default runtime limit is used + if (requestServicePoint.ConnectionLimit == RuntimeDefaultConnectionLimit) + { + requestServicePoint.ConnectionLimit = IncreasedConnectionLimit; + } + } + + public static void SetLimits(HttpClientHandler requestServicePoint) + { + // Only change when the default runtime limit is used + if (requestServicePoint.MaxConnectionsPerServer == RuntimeDefaultConnectionLimit) + { + requestServicePoint.MaxConnectionsPerServer = IncreasedConnectionLimit; + } + } + } +} \ No newline at end of file diff --git a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs index c44923068fd6..8e181df8ffff 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs @@ -8,12 +8,12 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; +using Azure.Core.TestFramework; using Microsoft.AspNetCore.Http; using NUnit.Framework; namespace Azure.Core.Tests { - [TestFixture(typeof(HttpClientTransport), true)] [TestFixture(typeof(HttpClientTransport), false)] #if NETFRAMEWORK @@ -158,6 +158,82 @@ public async Task NonBufferedFailedResponsesAreDisposedOf() Assert.Greater(reqNum, requestCount); } + [Test] + public async Task Opens50ParallelConnections() + { + // Running 50 sync requests on the threadpool would cause starvation + // and the test would take 20 sec to finish otherwise + ThreadPool.SetMinThreads(100, 100); + + HttpPipeline httpPipeline = HttpPipelineBuilder.Build(GetOptions()); + int reqNum = 0; + + TaskCompletionSource requestsTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using TestServer testServer = new TestServer( + async context => + { + if (Interlocked.Increment(ref reqNum) == 50) + { + requestsTcs.SetResult(true); + } + + await requestsTcs.Task; + }); + + var requestCount = 50; + List requests = new List(); + for (int i = 0; i < requestCount; i++) + { + HttpMessage message = httpPipeline.CreateMessage(); + message.Request.Uri.Reset(testServer.Address); + + requests.Add(Task.Run(() => ExecuteRequest(message, httpPipeline))); + } + + await Task.WhenAll(requests); + } + + [Test] + [Category("Live")] + public async Task Opens50ParallelConnectionsLive() + { + // Running 50 sync requests on the threadpool would cause starvation + // and the test would take 20 sec to finish otherwise + ThreadPool.SetMinThreads(100, 100); + + HttpPipeline httpPipeline = HttpPipelineBuilder.Build(GetOptions()); + int reqNum = 0; + + TaskCompletionSource requestsTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + async Task Connect() + { + using HttpMessage message = httpPipeline.CreateMessage(); + message.Request.Uri.Reset(new Uri("https://www.microsoft.com/")); + message.BufferResponse = false; + + await ExecuteRequest(message, httpPipeline); + + if (Interlocked.Increment(ref reqNum) == 50) + { + requestsTcs.SetResult(true); + } + + await requestsTcs.Task; + } + + var requestCount = 50; + List requests = new List(); + for (int i = 0; i < requestCount; i++) + { + + requests.Add(Task.Run(() => Connect())); + } + + await Task.WhenAll(requests); + } + [Test] public async Task BufferedResponsesReadableAfterMessageDisposed() { @@ -176,7 +252,6 @@ public async Task BufferedResponsesReadableAfterMessageDisposed() } }); - // Make sure we dispose things correctly and not exhaust the connection pool var requestCount = 100; for (int i = 0; i < requestCount; i++) {