diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln index d3087585f7..83594e3fea 100644 --- a/src/Middleware/Middleware.sln +++ b/src/Middleware/Middleware.sln @@ -293,6 +293,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Metada EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization", "..\Security\Authorization\Core\src\Microsoft.AspNetCore.Authorization.csproj", "{CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestThrottling.Microbenchmarks", "RequestThrottling\perf\Microbenchmarks\Microsoft.AspNetCore.RequestThrottling.Microbenchmarks.csproj", "{737B26B4-CFC6-4B44-9070-DD36334E85B3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1599,6 +1601,18 @@ Global {CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF}.Release|x64.Build.0 = Release|Any CPU {CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF}.Release|x86.ActiveCfg = Release|Any CPU {CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF}.Release|x86.Build.0 = Release|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|x64.Build.0 = Debug|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Debug|x86.Build.0 = Debug|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|Any CPU.Build.0 = Release|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|x64.ActiveCfg = Release|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|x64.Build.0 = Release|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|x86.ActiveCfg = Release|Any CPU + {737B26B4-CFC6-4B44-9070-DD36334E85B3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1725,6 +1739,7 @@ Global {353AA2B0-1013-486C-B5BD-9379385CA403} = {8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343} {7E2EA6E2-31FE-418A-9AE4-955A4C708AE7} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0} {CDDD7C43-5BEB-4E3E-8A59-FCDC83C9FBCF} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0} + {737B26B4-CFC6-4B44-9070-DD36334E85B3} = {8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83786312-A93B-4BB4-AB06-7C6913A59AFA} diff --git a/src/Middleware/RequestThrottling/RequestThrottling.slnf b/src/Middleware/RequestThrottling/RequestThrottling.slnf index 25a80d6b88..55e6a99067 100644 --- a/src/Middleware/RequestThrottling/RequestThrottling.slnf +++ b/src/Middleware/RequestThrottling/RequestThrottling.slnf @@ -18,7 +18,8 @@ "HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj", "RequestThrottling\\sample\\RequestThrottlingSample.csproj", "RequestThrottling\\src\\Microsoft.AspNetCore.RequestThrottling.csproj", - "RequestThrottling\\test\\Microsoft.AspNetCore.RequestThrottling.Tests.csproj" + "RequestThrottling\\test\\Microsoft.AspNetCore.RequestThrottling.Tests.csproj", + "RequestThrottling\\perf\\Microbenchmarks\\Microsoft.AspNetCore.RequestThrottling.Microbenchmarks.csproj" ] } } \ No newline at end of file diff --git a/src/Middleware/RequestThrottling/perf/Microbenchmarks/AssemblyInfo.cs b/src/Middleware/RequestThrottling/perf/Microbenchmarks/AssemblyInfo.cs new file mode 100644 index 0000000000..32248e0d1b --- /dev/null +++ b/src/Middleware/RequestThrottling/perf/Microbenchmarks/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark] diff --git a/src/Middleware/RequestThrottling/perf/Microbenchmarks/Microsoft.AspNetCore.RequestThrottling.Microbenchmarks.csproj b/src/Middleware/RequestThrottling/perf/Microbenchmarks/Microsoft.AspNetCore.RequestThrottling.Microbenchmarks.csproj new file mode 100644 index 0000000000..fa0c7dd9f5 --- /dev/null +++ b/src/Middleware/RequestThrottling/perf/Microbenchmarks/Microsoft.AspNetCore.RequestThrottling.Microbenchmarks.csproj @@ -0,0 +1,14 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + diff --git a/src/Middleware/RequestThrottling/perf/Microbenchmarks/QueueEmptyOverhead.cs b/src/Middleware/RequestThrottling/perf/Microbenchmarks/QueueEmptyOverhead.cs new file mode 100644 index 0000000000..ffde38c86a --- /dev/null +++ b/src/Middleware/RequestThrottling/perf/Microbenchmarks/QueueEmptyOverhead.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.RequestThrottling.Microbenchmarks +{ + public class QueueEmptyOverhead + { + private const int _numRequests = 20000; + + private RequestThrottlingMiddleware _middleware; + private RequestDelegate _restOfServer; + + [GlobalSetup] + public void GlobalSetup() + { + _restOfServer = YieldsThreadInternally ? (RequestDelegate)YieldsThread : (RequestDelegate)CompletesImmediately; + + var options = new RequestThrottlingOptions + { + MaxConcurrentRequests = 8, + RequestQueueLimit = _numRequests + }; + + _middleware = new RequestThrottlingMiddleware( + next: _restOfServer, + loggerFactory: NullLoggerFactory.Instance, + options: Options.Create(options) + ); + } + + [Params(false, true)] + public bool YieldsThreadInternally; + + [Benchmark(OperationsPerInvoke = _numRequests)] + public async Task Baseline() + { + for (int i = 0; i < _numRequests; i++) + { + await _restOfServer(null); + } + } + + [Benchmark(OperationsPerInvoke = _numRequests)] + public async Task WithEmptyQueueOverhead() + { + for (int i = 0; i < _numRequests; i++) + { + await _middleware.Invoke(null); + } + } + + private static async Task YieldsThread(HttpContext context) + { + await Task.Yield(); + } + + private static Task CompletesImmediately(HttpContext context) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Middleware/RequestThrottling/perf/Microbenchmarks/QueueFullOverhead.cs b/src/Middleware/RequestThrottling/perf/Microbenchmarks/QueueFullOverhead.cs new file mode 100644 index 0000000000..eb4fa7c7de --- /dev/null +++ b/src/Middleware/RequestThrottling/perf/Microbenchmarks/QueueFullOverhead.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.RequestThrottling.Microbenchmarks +{ + public class QueueFullOverhead + { + private const int _numRequests = 2000; + private int _requestCount = 0; + private ManualResetEventSlim _mres = new ManualResetEventSlim(); + + private RequestThrottlingMiddleware _middleware; + + [Params(8)] + public int MaxConcurrentRequests; + + [GlobalSetup] + public void GlobalSetup() + { + var options = new RequestThrottlingOptions + { + MaxConcurrentRequests = MaxConcurrentRequests, + RequestQueueLimit = _numRequests + }; + + _middleware = new RequestThrottlingMiddleware( + next: (RequestDelegate)_incrementAndCheck, + loggerFactory: NullLoggerFactory.Instance, + options: Options.Create(options) + ); + } + + [IterationSetup] + public void Setup() + { + _requestCount = 0; + _mres.Reset(); + } + + private async Task _incrementAndCheck(HttpContext context) + { + if (Interlocked.Increment(ref _requestCount) == _numRequests) + { + _mres.Set(); + } + + await Task.Yield(); + } + + [Benchmark(OperationsPerInvoke = _numRequests)] + public void Baseline() + { + for (int i = 0; i < _numRequests; i++) + { + _ = _incrementAndCheck(null); + } + + _mres.Wait(); + } + + [Benchmark(OperationsPerInvoke = _numRequests)] + public void QueueingAll() + { + for (int i = 0; i < _numRequests; i++) + { + _ = _middleware.Invoke(null); + } + + _mres.Wait(); + } + } +} diff --git a/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs index ae4e1ab1a7..55a8fa46aa 100644 --- a/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs +++ b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs @@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.RequestThrottling public partial class RequestThrottlingMiddleware { public RequestThrottlingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions options) { } + public int ActiveRequestCount { get { throw null; } } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; } } diff --git a/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj b/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj index 9f49f115c0..4b8a97aae2 100644 --- a/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj +++ b/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj @@ -1,13 +1,20 @@ - + netcoreapp3.0 - + + + + + + + + diff --git a/src/Middleware/RequestThrottling/src/Internal/IRequestQueue.cs b/src/Middleware/RequestThrottling/src/Internal/IRequestQueue.cs new file mode 100644 index 0000000000..f9fb0484d8 --- /dev/null +++ b/src/Middleware/RequestThrottling/src/Internal/IRequestQueue.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.RequestThrottling.Internal +{ + interface IRequestQueue : IDisposable + { + int TotalRequests { get; } + + Task TryEnterQueueAsync(); + + void Release(); + } +} diff --git a/src/Middleware/RequestThrottling/src/Internal/RequestQueue.cs b/src/Middleware/RequestThrottling/src/Internal/TailDrop.cs similarity index 93% rename from src/Middleware/RequestThrottling/src/Internal/RequestQueue.cs rename to src/Middleware/RequestThrottling/src/Internal/TailDrop.cs index 6886b24ee4..83b9ef615e 100644 --- a/src/Middleware/RequestThrottling/src/Internal/RequestQueue.cs +++ b/src/Middleware/RequestThrottling/src/Internal/TailDrop.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Microsoft.AspNetCore.RequestThrottling.Internal { - internal class RequestQueue : IDisposable + internal class TailDrop : IRequestQueue { private readonly int _maxConcurrentRequests; private readonly int _requestQueueLimit; @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.RequestThrottling.Internal private object _totalRequestsLock = new object(); public int TotalRequests { get; private set; } - public RequestQueue(int maxConcurrentRequests, int requestQueueLimit) + public TailDrop(int maxConcurrentRequests, int requestQueueLimit) { _maxConcurrentRequests = maxConcurrentRequests; _requestQueueLimit = requestQueueLimit; diff --git a/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj b/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj index 0090f373c0..75b180f978 100644 --- a/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj +++ b/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj @@ -1,10 +1,11 @@ - + ASP.NET Core middleware for queuing incoming HTTP requests, to avoid threadpool starvation. netcoreapp3.0 true aspnetcore;queue;queuing + true diff --git a/src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs b/src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs index 7af7e0603c..3d535f48c0 100644 --- a/src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs +++ b/src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.RequestThrottling /// public class RequestThrottlingMiddleware { - private readonly RequestQueue _requestQueue; + private readonly IRequestQueue _requestQueue; private readonly RequestDelegate _next; private readonly RequestThrottlingOptions _requestThrottlingOptions; private readonly ILogger _logger; @@ -35,9 +35,9 @@ namespace Microsoft.AspNetCore.RequestThrottling { throw new ArgumentException("The value of 'options.MaxConcurrentRequests' must be specified.", nameof(options)); } - if (_requestThrottlingOptions.MaxConcurrentRequests < 0) + if (_requestThrottlingOptions.MaxConcurrentRequests <= 0) { - throw new ArgumentException("The value of 'options.MaxConcurrentRequests' must be a positive integer.", nameof(options)); + throw new ArgumentOutOfRangeException(nameof(options), "The value of `options.MaxConcurrentRequests` must be a positive integer."); } if (_requestThrottlingOptions.RequestQueueLimit < 0) { @@ -51,9 +51,16 @@ namespace Microsoft.AspNetCore.RequestThrottling _next = next; _logger = loggerFactory.CreateLogger(); - _requestQueue = new RequestQueue( - _requestThrottlingOptions.MaxConcurrentRequests.Value, - _requestThrottlingOptions.RequestQueueLimit); + + if (_requestThrottlingOptions.ServerAlwaysBlocks) + { + // note: this option for testing only. Blocks all requests from entering the server. + _requestQueue = new TailDrop(0, _requestThrottlingOptions.RequestQueueLimit); + } + else + { + _requestQueue = new TailDrop(_requestThrottlingOptions.MaxConcurrentRequests.Value, _requestThrottlingOptions.RequestQueueLimit); + } } /// @@ -64,25 +71,25 @@ namespace Microsoft.AspNetCore.RequestThrottling public async Task Invoke(HttpContext context) { var waitInQueueTask = _requestQueue.TryEnterQueueAsync(); - if (waitInQueueTask.IsCompletedSuccessfully && !waitInQueueTask.Result) + + if (waitInQueueTask.IsCompletedSuccessfully && waitInQueueTask.Result) + { + RequestThrottlingLog.RequestRunImmediately(_logger, ActiveRequestCount); + } + else + { + RequestThrottlingLog.RequestEnqueued(_logger, ActiveRequestCount); + await waitInQueueTask; + RequestThrottlingLog.RequestDequeued(_logger, ActiveRequestCount); + } + + if (!waitInQueueTask.Result) { RequestThrottlingLog.RequestRejectedQueueFull(_logger); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; await _requestThrottlingOptions.OnRejected(context); return; } - else if (!waitInQueueTask.IsCompletedSuccessfully) - { - RequestThrottlingLog.RequestEnqueued(_logger, ActiveRequestCount); - var result = await waitInQueueTask; - RequestThrottlingLog.RequestDequeued(_logger, ActiveRequestCount); - - Debug.Assert(result); - } - else - { - RequestThrottlingLog.RequestRunImmediately(_logger, ActiveRequestCount); - } try { @@ -98,7 +105,7 @@ namespace Microsoft.AspNetCore.RequestThrottling /// The number of requests currently on the server. /// Cannot exceeed the sum of and />. /// - internal int ActiveRequestCount + public int ActiveRequestCount { get => _requestQueue.TotalRequests; } diff --git a/src/Middleware/RequestThrottling/src/RequestThrottlingOptions.cs b/src/Middleware/RequestThrottling/src/RequestThrottlingOptions.cs index c0e03ae2a1..317fc04691 100644 --- a/src/Middleware/RequestThrottling/src/RequestThrottlingOptions.cs +++ b/src/Middleware/RequestThrottling/src/RequestThrottlingOptions.cs @@ -33,5 +33,10 @@ namespace Microsoft.AspNetCore.RequestThrottling { return Task.CompletedTask; }; + + /// + /// For internal testing only. If true, no requests will enter the server. + /// + internal bool ServerAlwaysBlocks { get; set; } = false; } } diff --git a/src/Middleware/RequestThrottling/test/MiddlewareTests.cs b/src/Middleware/RequestThrottling/test/MiddlewareTests.cs index aed8d8a963..13d6c8b26d 100644 --- a/src/Middleware/RequestThrottling/test/MiddlewareTests.cs +++ b/src/Middleware/RequestThrottling/test/MiddlewareTests.cs @@ -84,8 +84,7 @@ namespace Microsoft.AspNetCore.RequestThrottling.Tests [Fact] public async void RequestsBlockedIfQueueFull() { - var middleware = TestUtils.CreateTestMiddleware( - maxConcurrentRequests: 0, + var middleware = TestUtils.CreateBlockingTestMiddleware( requestQueueLimit: 0, next: httpContext => { @@ -99,9 +98,7 @@ namespace Microsoft.AspNetCore.RequestThrottling.Tests [Fact] public async void FullQueueResultsIn503Error() { - var middleware = TestUtils.CreateTestMiddleware( - maxConcurrentRequests: 0, - requestQueueLimit: 0); + var middleware = TestUtils.CreateBlockingTestMiddleware(requestQueueLimit: 0); var context = new DefaultHttpContext(); await middleware.Invoke(context).OrTimeout(); @@ -112,7 +109,7 @@ namespace Microsoft.AspNetCore.RequestThrottling.Tests public void MultipleRequestsFillUpQueue() { var middleware = TestUtils.CreateTestMiddleware( - maxConcurrentRequests: 0, + maxConcurrentRequests: 1, requestQueueLimit: 10, next: httpContext => { @@ -133,8 +130,7 @@ namespace Microsoft.AspNetCore.RequestThrottling.Tests { bool onRejectedInvoked = false; - var middleware = TestUtils.CreateTestMiddleware( - maxConcurrentRequests: 0, + var middleware = TestUtils.CreateBlockingTestMiddleware( requestQueueLimit: 0, onRejected: httpContext => { diff --git a/src/Middleware/RequestThrottling/test/TestUtils.cs b/src/Middleware/RequestThrottling/test/TestUtils.cs index 9b5380f0ac..b07a552cfc 100644 --- a/src/Middleware/RequestThrottling/test/TestUtils.cs +++ b/src/Middleware/RequestThrottling/test/TestUtils.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Threading.Tasks; -using Microsoft.AspNetCore.RequestThrottling; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -20,6 +19,23 @@ namespace Microsoft.AspNetCore.RequestThrottling.Tests RequestQueueLimit = requestQueueLimit }; + return BuildFromOptions(options, onRejected, next); + } + + public static RequestThrottlingMiddleware CreateBlockingTestMiddleware(int requestQueueLimit = 5000, RequestDelegate onRejected = null, RequestDelegate next = null) + { + var options = new RequestThrottlingOptions + { + MaxConcurrentRequests = 999, + RequestQueueLimit = requestQueueLimit, + ServerAlwaysBlocks = true + }; + + return BuildFromOptions(options, onRejected, next); + } + + private static RequestThrottlingMiddleware BuildFromOptions(RequestThrottlingOptions options, RequestDelegate onRejected, RequestDelegate next) + { if (onRejected != null) { options.OnRejected = onRejected; @@ -32,6 +48,6 @@ namespace Microsoft.AspNetCore.RequestThrottling.Tests ); } - internal static RequestQueue CreateRequestQueue(int maxConcurrentRequests) => new RequestQueue(maxConcurrentRequests, 5000); + internal static IRequestQueue CreateRequestQueue(int maxConcurrentRequests) => new TailDrop(maxConcurrentRequests, 5000); } }