Skip to content

Commit 92d72c3

Browse files
authored
Increase connection pool size limit automatically (#15263)
1 parent 5bbe795 commit 92d72c3

File tree

5 files changed

+119
-2
lines changed

5 files changed

+119
-2
lines changed

sdk/core/Azure.Core/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 1.6.0-beta.1 (Unreleased)
44

5+
### Changed
6+
- `ServicePointManager` Connection limit is automatically increased to `50` for Azure endpoints.
7+
58

69
## 1.5.0 (2020-09-03)
710

sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ private static HttpClient CreateDefaultClient()
8686
httpClientHandler.Proxy = webProxy;
8787
}
8888

89+
#if NETFRAMEWORK
90+
ServicePointHelpers.SetLimits(httpClientHandler);
91+
#endif
92+
8993
return new HttpClient(httpClientHandler);
9094
}
9195

sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public override async ValueTask ProcessAsync(HttpMessage message)
4848
private async ValueTask ProcessInternal(HttpMessage message, bool async)
4949
{
5050
var request = CreateRequest(message.Request);
51+
52+
ServicePointHelpers.SetLimits(request.ServicePoint);
53+
5154
using var registration = message.CancellationToken.Register(state => ((HttpWebRequest) state).Abort(), request);
5255
try
5356
{
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Net;
5+
using System.Net.Http;
6+
7+
namespace Azure.Core.Pipeline
8+
{
9+
internal static class ServicePointHelpers
10+
{
11+
private const int RuntimeDefaultConnectionLimit = 2;
12+
private const int IncreasedConnectionLimit = 50;
13+
14+
public static void SetLimits(ServicePoint requestServicePoint)
15+
{
16+
// Only change when the default runtime limit is used
17+
if (requestServicePoint.ConnectionLimit == RuntimeDefaultConnectionLimit)
18+
{
19+
requestServicePoint.ConnectionLimit = IncreasedConnectionLimit;
20+
}
21+
}
22+
23+
public static void SetLimits(HttpClientHandler requestServicePoint)
24+
{
25+
// Only change when the default runtime limit is used
26+
if (requestServicePoint.MaxConnectionsPerServer == RuntimeDefaultConnectionLimit)
27+
{
28+
requestServicePoint.MaxConnectionsPerServer = IncreasedConnectionLimit;
29+
}
30+
}
31+
}
32+
}

sdk/core/Azure.Core/tests/HttpPipelineFunctionalTests.cs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using Azure.Core.Pipeline;
11+
using Azure.Core.TestFramework;
1112
using Microsoft.AspNetCore.Http;
1213
using NUnit.Framework;
1314

1415
namespace Azure.Core.Tests
1516
{
16-
1717
[TestFixture(typeof(HttpClientTransport), true)]
1818
[TestFixture(typeof(HttpClientTransport), false)]
1919
#if NETFRAMEWORK
@@ -158,6 +158,82 @@ public async Task NonBufferedFailedResponsesAreDisposedOf()
158158
Assert.Greater(reqNum, requestCount);
159159
}
160160

161+
[Test]
162+
public async Task Opens50ParallelConnections()
163+
{
164+
// Running 50 sync requests on the threadpool would cause starvation
165+
// and the test would take 20 sec to finish otherwise
166+
ThreadPool.SetMinThreads(100, 100);
167+
168+
HttpPipeline httpPipeline = HttpPipelineBuilder.Build(GetOptions());
169+
int reqNum = 0;
170+
171+
TaskCompletionSource<object> requestsTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
172+
173+
using TestServer testServer = new TestServer(
174+
async context =>
175+
{
176+
if (Interlocked.Increment(ref reqNum) == 50)
177+
{
178+
requestsTcs.SetResult(true);
179+
}
180+
181+
await requestsTcs.Task;
182+
});
183+
184+
var requestCount = 50;
185+
List<Task> requests = new List<Task>();
186+
for (int i = 0; i < requestCount; i++)
187+
{
188+
HttpMessage message = httpPipeline.CreateMessage();
189+
message.Request.Uri.Reset(testServer.Address);
190+
191+
requests.Add(Task.Run(() => ExecuteRequest(message, httpPipeline)));
192+
}
193+
194+
await Task.WhenAll(requests);
195+
}
196+
197+
[Test]
198+
[Category("Live")]
199+
public async Task Opens50ParallelConnectionsLive()
200+
{
201+
// Running 50 sync requests on the threadpool would cause starvation
202+
// and the test would take 20 sec to finish otherwise
203+
ThreadPool.SetMinThreads(100, 100);
204+
205+
HttpPipeline httpPipeline = HttpPipelineBuilder.Build(GetOptions());
206+
int reqNum = 0;
207+
208+
TaskCompletionSource<object> requestsTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
209+
210+
async Task Connect()
211+
{
212+
using HttpMessage message = httpPipeline.CreateMessage();
213+
message.Request.Uri.Reset(new Uri("https://www.microsoft.com/"));
214+
message.BufferResponse = false;
215+
216+
await ExecuteRequest(message, httpPipeline);
217+
218+
if (Interlocked.Increment(ref reqNum) == 50)
219+
{
220+
requestsTcs.SetResult(true);
221+
}
222+
223+
await requestsTcs.Task;
224+
}
225+
226+
var requestCount = 50;
227+
List<Task> requests = new List<Task>();
228+
for (int i = 0; i < requestCount; i++)
229+
{
230+
231+
requests.Add(Task.Run(() => Connect()));
232+
}
233+
234+
await Task.WhenAll(requests);
235+
}
236+
161237
[Test]
162238
public async Task BufferedResponsesReadableAfterMessageDisposed()
163239
{
@@ -176,7 +252,6 @@ public async Task BufferedResponsesReadableAfterMessageDisposed()
176252
}
177253
});
178254

179-
// Make sure we dispose things correctly and not exhaust the connection pool
180255
var requestCount = 100;
181256
for (int i = 0; i < requestCount; i++)
182257
{

0 commit comments

Comments
 (0)