Skip to content

Commit 844530d

Browse files
authored
Overhead Benchmark for Request Throttling (#10907)
* initial implementation; no tests * benchmark project * Better benchmarks * overhead test for congested queue * Addressed feedback
1 parent af812f2 commit 844530d

15 files changed

+265
-34
lines changed

src/Middleware/Middleware.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Metada
293293
EndProject
294294
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization", "..\Security\Authorization\Core\src\Microsoft.AspNetCore.Authorization.csproj", "{CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF}"
295295
EndProject
296+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestThrottling.Microbenchmarks", "RequestThrottling\perf\Microbenchmarks\Microsoft.AspNetCore.RequestThrottling.Microbenchmarks.csproj", "{737B26B4-CFC6-4B44-9070-DD36334E85B3}"
297+
EndProject
296298
Global
297299
GlobalSection(SolutionConfigurationPlatforms) = preSolution
298300
Debug|Any CPU = Debug|Any CPU
@@ -1599,6 +1601,18 @@ Global
15991601
{CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF}.Release|x64.Build.0 = Release|Any CPU
16001602
{CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF}.Release|x86.ActiveCfg = Release|Any CPU
16011603
{CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF}.Release|x86.Build.0 = Release|Any CPU
1604+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1605+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
1606+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|x64.ActiveCfg = Debug|Any CPU
1607+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|x64.Build.0 = Debug|Any CPU
1608+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|x86.ActiveCfg = Debug|Any CPU
1609+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|x86.Build.0 = Debug|Any CPU
1610+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
1611+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|Any CPU.Build.0 = Release|Any CPU
1612+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|x64.ActiveCfg = Release|Any CPU
1613+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|x64.Build.0 = Release|Any CPU
1614+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|x86.ActiveCfg = Release|Any CPU
1615+
{737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|x86.Build.0 = Release|Any CPU
16021616
EndGlobalSection
16031617
GlobalSection(SolutionProperties) = preSolution
16041618
HideSolutionNode = FALSE
@@ -1725,6 +1739,7 @@ Global
17251739
{353AA2B0-1013-486C-B5BD-9379385CA403} = {8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343}
17261740
{7E2EA6E2-31FE-418A-9AE4-955A4C708AE7} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0}
17271741
{CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0}
1742+
{737B26B4-CFC6-4B44-9070-DD36334E85B3} = {8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343}
17281743
EndGlobalSection
17291744
GlobalSection(ExtensibilityGlobals) = postSolution
17301745
SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA}

src/Middleware/RequestThrottling/RequestThrottling.slnf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj",
1919
"RequestThrottling\\sample\\RequestThrottlingSample.csproj",
2020
"RequestThrottling\\src\\Microsoft.AspNetCore.RequestThrottling.csproj",
21-
"RequestThrottling\\test\\Microsoft.AspNetCore.RequestThrottling.Tests.csproj"
21+
"RequestThrottling\\test\\Microsoft.AspNetCore.RequestThrottling.Tests.csproj",
22+
"RequestThrottling\\perf\\Microbenchmarks\\Microsoft.AspNetCore.RequestThrottling.Microbenchmarks.csproj"
2223
]
2324
}
2425
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp3.0</TargetFramework>
6+
<!--<StartupObject>Microsoft.AspNetCore.RequestThrottling.Microbenchmarks.Test</StartupObject>-->
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<Reference Include="BenchmarkDotNet" />
11+
<Reference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" />
12+
<Reference Include="Microsoft.AspNetCore.RequestThrottling" />
13+
</ItemGroup>
14+
</Project>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using BenchmarkDotNet.Attributes;
8+
using BenchmarkDotNet.Running;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.Extensions.Logging.Abstractions;
11+
using Microsoft.Extensions.Options;
12+
13+
namespace Microsoft.AspNetCore.RequestThrottling.Microbenchmarks
14+
{
15+
public class QueueEmptyOverhead
16+
{
17+
private const int _numRequests = 20000;
18+
19+
private RequestThrottlingMiddleware _middleware;
20+
private RequestDelegate _restOfServer;
21+
22+
[GlobalSetup]
23+
public void GlobalSetup()
24+
{
25+
_restOfServer = YieldsThreadInternally ? (RequestDelegate)YieldsThread : (RequestDelegate)CompletesImmediately;
26+
27+
var options = new RequestThrottlingOptions
28+
{
29+
MaxConcurrentRequests = 8,
30+
RequestQueueLimit = _numRequests
31+
};
32+
33+
_middleware = new RequestThrottlingMiddleware(
34+
next: _restOfServer,
35+
loggerFactory: NullLoggerFactory.Instance,
36+
options: Options.Create(options)
37+
);
38+
}
39+
40+
[Params(false, true)]
41+
public bool YieldsThreadInternally;
42+
43+
[Benchmark(OperationsPerInvoke = _numRequests)]
44+
public async Task Baseline()
45+
{
46+
for (int i = 0; i < _numRequests; i++)
47+
{
48+
await _restOfServer(null);
49+
}
50+
}
51+
52+
[Benchmark(OperationsPerInvoke = _numRequests)]
53+
public async Task WithEmptyQueueOverhead()
54+
{
55+
for (int i = 0; i < _numRequests; i++)
56+
{
57+
await _middleware.Invoke(null);
58+
}
59+
}
60+
61+
private static async Task YieldsThread(HttpContext context)
62+
{
63+
await Task.Yield();
64+
}
65+
66+
private static Task CompletesImmediately(HttpContext context)
67+
{
68+
return Task.CompletedTask;
69+
}
70+
}
71+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using BenchmarkDotNet.Attributes;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.Extensions.Logging.Abstractions;
9+
using Microsoft.Extensions.Options;
10+
11+
namespace Microsoft.AspNetCore.RequestThrottling.Microbenchmarks
12+
{
13+
public class QueueFullOverhead
14+
{
15+
private const int _numRequests = 2000;
16+
private int _requestCount = 0;
17+
private ManualResetEventSlim _mres = new ManualResetEventSlim();
18+
19+
private RequestThrottlingMiddleware _middleware;
20+
21+
[Params(8)]
22+
public int MaxConcurrentRequests;
23+
24+
[GlobalSetup]
25+
public void GlobalSetup()
26+
{
27+
var options = new RequestThrottlingOptions
28+
{
29+
MaxConcurrentRequests = MaxConcurrentRequests,
30+
RequestQueueLimit = _numRequests
31+
};
32+
33+
_middleware = new RequestThrottlingMiddleware(
34+
next: (RequestDelegate)_incrementAndCheck,
35+
loggerFactory: NullLoggerFactory.Instance,
36+
options: Options.Create(options)
37+
);
38+
}
39+
40+
[IterationSetup]
41+
public void Setup()
42+
{
43+
_requestCount = 0;
44+
_mres.Reset();
45+
}
46+
47+
private async Task _incrementAndCheck(HttpContext context)
48+
{
49+
if (Interlocked.Increment(ref _requestCount) == _numRequests)
50+
{
51+
_mres.Set();
52+
}
53+
54+
await Task.Yield();
55+
}
56+
57+
[Benchmark(OperationsPerInvoke = _numRequests)]
58+
public void Baseline()
59+
{
60+
for (int i = 0; i < _numRequests; i++)
61+
{
62+
_ = _incrementAndCheck(null);
63+
}
64+
65+
_mres.Wait();
66+
}
67+
68+
[Benchmark(OperationsPerInvoke = _numRequests)]
69+
public void QueueingAll()
70+
{
71+
for (int i = 0; i < _numRequests; i++)
72+
{
73+
_ = _middleware.Invoke(null);
74+
}
75+
76+
_mres.Wait();
77+
}
78+
}
79+
}

src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.RequestThrottling
1313
public partial class RequestThrottlingMiddleware
1414
{
1515
public RequestThrottlingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.RequestThrottling.RequestThrottlingOptions> options) { }
16+
public int ActiveRequestCount { get { throw null; } }
1617
[System.Diagnostics.DebuggerStepThroughAttribute]
1718
public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; }
1819
}
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
44
<TargetFramework>netcoreapp3.0</TargetFramework>
55
</PropertyGroup>
66

7-
<ItemGroup>
7+
<ItemGroup Condition="'$(BenchmarksTargetFramework)' == ''">
88
<Reference Include="Microsoft.Extensions.Logging.Console" />
99
<Reference Include="Microsoft.AspNetCore.RequestThrottling" />
1010
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
1111
</ItemGroup>
1212

13+
<ItemGroup Condition="'$(BenchmarksTargetFramework)' != ''">
14+
<PackageReference Include="Microsoft.AspNetCore.RequestThrottling" Version="$(MicrosoftAspNetCoreAppPackageVersion)" />
15+
16+
<FrameworkReference Update="Microsoft.AspNetCore.App" RuntimeFrameworkVersion="$(MicrosoftAspNetCoreAppPackageVersion)" />
17+
<FrameworkReference Update="Microsoft.NETCore.App" RuntimeFrameworkVersion="$(MicrosoftNETCoreAppPackageVersion)" />
18+
</ItemGroup>
19+
1320
</Project>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.AspNetCore.RequestThrottling.Internal
8+
{
9+
interface IRequestQueue : IDisposable
10+
{
11+
int TotalRequests { get; }
12+
13+
Task<bool> TryEnterQueueAsync();
14+
15+
void Release();
16+
}
17+
}

src/Middleware/RequestThrottling/src/Internal/RequestQueue.cs renamed to src/Middleware/RequestThrottling/src/Internal/TailDrop.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace Microsoft.AspNetCore.RequestThrottling.Internal
99
{
10-
internal class RequestQueue : IDisposable
10+
internal class TailDrop : IRequestQueue
1111
{
1212
private readonly int _maxConcurrentRequests;
1313
private readonly int _requestQueueLimit;
@@ -16,7 +16,7 @@ internal class RequestQueue : IDisposable
1616
private object _totalRequestsLock = new object();
1717
public int TotalRequests { get; private set; }
1818

19-
public RequestQueue(int maxConcurrentRequests, int requestQueueLimit)
19+
public TailDrop(int maxConcurrentRequests, int requestQueueLimit)
2020
{
2121
_maxConcurrentRequests = maxConcurrentRequests;
2222
_requestQueueLimit = requestQueueLimit;

src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<Description>ASP.NET Core middleware for queuing incoming HTTP requests, to avoid threadpool starvation.</Description>
55
<TargetFramework>netcoreapp3.0</TargetFramework>
66
<GenerateDocumentationFile>true</GenerateDocumentationFile>
77
<PackageTags>aspnetcore;queue;queuing</PackageTags>
8+
<IsShippingPackage>true</IsShippingPackage>
89
</PropertyGroup>
910

1011
<ItemGroup>

src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.RequestThrottling
1616
/// </summary>
1717
public class RequestThrottlingMiddleware
1818
{
19-
private readonly RequestQueue _requestQueue;
19+
private readonly IRequestQueue _requestQueue;
2020
private readonly RequestDelegate _next;
2121
private readonly RequestThrottlingOptions _requestThrottlingOptions;
2222
private readonly ILogger _logger;
@@ -35,9 +35,9 @@ public RequestThrottlingMiddleware(RequestDelegate next, ILoggerFactory loggerFa
3535
{
3636
throw new ArgumentException("The value of 'options.MaxConcurrentRequests' must be specified.", nameof(options));
3737
}
38-
if (_requestThrottlingOptions.MaxConcurrentRequests < 0)
38+
if (_requestThrottlingOptions.MaxConcurrentRequests <= 0)
3939
{
40-
throw new ArgumentException("The value of 'options.MaxConcurrentRequests' must be a positive integer.", nameof(options));
40+
throw new ArgumentOutOfRangeException(nameof(options), "The value of `options.MaxConcurrentRequests` must be a positive integer.");
4141
}
4242
if (_requestThrottlingOptions.RequestQueueLimit < 0)
4343
{
@@ -51,9 +51,16 @@ public RequestThrottlingMiddleware(RequestDelegate next, ILoggerFactory loggerFa
5151

5252
_next = next;
5353
_logger = loggerFactory.CreateLogger<RequestThrottlingMiddleware>();
54-
_requestQueue = new RequestQueue(
55-
_requestThrottlingOptions.MaxConcurrentRequests.Value,
56-
_requestThrottlingOptions.RequestQueueLimit);
54+
55+
if (_requestThrottlingOptions.ServerAlwaysBlocks)
56+
{
57+
// note: this option for testing only. Blocks all requests from entering the server.
58+
_requestQueue = new TailDrop(0, _requestThrottlingOptions.RequestQueueLimit);
59+
}
60+
else
61+
{
62+
_requestQueue = new TailDrop(_requestThrottlingOptions.MaxConcurrentRequests.Value, _requestThrottlingOptions.RequestQueueLimit);
63+
}
5764
}
5865

5966
/// <summary>
@@ -64,24 +71,24 @@ public RequestThrottlingMiddleware(RequestDelegate next, ILoggerFactory loggerFa
6471
public async Task Invoke(HttpContext context)
6572
{
6673
var waitInQueueTask = _requestQueue.TryEnterQueueAsync();
67-
if (waitInQueueTask.IsCompletedSuccessfully && !waitInQueueTask.Result)
74+
75+
if (waitInQueueTask.IsCompletedSuccessfully && waitInQueueTask.Result)
6876
{
69-
RequestThrottlingLog.RequestRejectedQueueFull(_logger);
70-
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
71-
await _requestThrottlingOptions.OnRejected(context);
72-
return;
77+
RequestThrottlingLog.RequestRunImmediately(_logger, ActiveRequestCount);
7378
}
74-
else if (!waitInQueueTask.IsCompletedSuccessfully)
79+
else
7580
{
7681
RequestThrottlingLog.RequestEnqueued(_logger, ActiveRequestCount);
77-
var result = await waitInQueueTask;
82+
await waitInQueueTask;
7883
RequestThrottlingLog.RequestDequeued(_logger, ActiveRequestCount);
79-
80-
Debug.Assert(result);
8184
}
82-
else
85+
86+
if (!waitInQueueTask.Result)
8387
{
84-
RequestThrottlingLog.RequestRunImmediately(_logger, ActiveRequestCount);
88+
RequestThrottlingLog.RequestRejectedQueueFull(_logger);
89+
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
90+
await _requestThrottlingOptions.OnRejected(context);
91+
return;
8592
}
8693

8794
try
@@ -98,7 +105,7 @@ public async Task Invoke(HttpContext context)
98105
/// The number of requests currently on the server.
99106
/// Cannot exceeed the sum of <see cref="RequestThrottlingOptions.RequestQueueLimit"> and </see>/><see cref="RequestThrottlingOptions.MaxConcurrentRequests"/>.
100107
/// </summary>
101-
internal int ActiveRequestCount
108+
public int ActiveRequestCount
102109
{
103110
get => _requestQueue.TotalRequests;
104111
}

src/Middleware/RequestThrottling/src/RequestThrottlingOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,10 @@ public class RequestThrottlingOptions
3333
{
3434
return Task.CompletedTask;
3535
};
36+
37+
/// <summary>
38+
/// For internal testing only. If true, no requests will enter the server.
39+
/// </summary>
40+
internal bool ServerAlwaysBlocks { get; set; } = false;
3641
}
3742
}

0 commit comments

Comments
 (0)