diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 092cfa3472..0ac8db756b 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -83,6 +83,7 @@ + diff --git a/src/Middleware/Middleware.sln b/src/Middleware/Middleware.sln index 5f16259900..a02c8c40a8 100644 --- a/src/Middleware/Middleware.sln +++ b/src/Middleware/Middleware.sln @@ -283,6 +283,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HeaderPropagationSample", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IIS", "..\Servers\IIS\IIS\src\Microsoft.AspNetCore.Server.IIS.csproj", "{B9BE1823-B555-4AAB-AEBC-C8C3F48C8861}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RequestThrottling", "RequestThrottling", "{8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RequestThrottlingSample", "RequestThrottling\sample\RequestThrottlingSample.csproj", "{6720919C-0DEA-49E1-90DC-F1883F7919CD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestThrottling", "RequestThrottling\src\Microsoft.AspNetCore.RequestThrottling.csproj", "{4CE2384D-6B88-4824-ADD1-4183D180FEFF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestThrottling.Tests", "RequestThrottling\test\Microsoft.AspNetCore.RequestThrottling.Tests.csproj", "{353AA2B0-1013-486C-B5BD-9379385CA403}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1541,6 +1549,42 @@ Global {B9BE1823-B555-4AAB-AEBC-C8C3F48C8861}.Release|x64.Build.0 = Release|Any CPU {B9BE1823-B555-4AAB-AEBC-C8C3F48C8861}.Release|x86.ActiveCfg = Release|Any CPU {B9BE1823-B555-4AAB-AEBC-C8C3F48C8861}.Release|x86.Build.0 = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|x64.Build.0 = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Debug|x86.Build.0 = Debug|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|Any CPU.Build.0 = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|x64.ActiveCfg = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|x64.Build.0 = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|x86.ActiveCfg = Release|Any CPU + {6720919C-0DEA-49E1-90DC-F1883F7919CD}.Release|x86.Build.0 = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|x64.ActiveCfg = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|x64.Build.0 = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|x86.ActiveCfg = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Debug|x86.Build.0 = Debug|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|Any CPU.Build.0 = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|x64.ActiveCfg = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|x64.Build.0 = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|x86.ActiveCfg = Release|Any CPU + {4CE2384D-6B88-4824-ADD1-4183D180FEFF}.Release|x86.Build.0 = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|Any CPU.Build.0 = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|x64.ActiveCfg = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|x64.Build.0 = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|x86.ActiveCfg = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Debug|x86.Build.0 = Debug|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|Any CPU.ActiveCfg = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|Any CPU.Build.0 = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|x64.ActiveCfg = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|x64.Build.0 = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|x86.ActiveCfg = Release|Any CPU + {353AA2B0-1013-486C-B5BD-9379385CA403}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1663,6 +1707,9 @@ Global {179A159B-87EA-4353-BE92-4FB6CC05BC7D} = {0437D207-864E-429C-92B4-9D08D290188C} {CDE2E736-A034-4748-98C4-0DEDAAC8063D} = {179A159B-87EA-4353-BE92-4FB6CC05BC7D} {B9BE1823-B555-4AAB-AEBC-C8C3F48C8861} = {ACA6DDB9-7592-47CE-A740-D15BF307E9E0} + {6720919C-0DEA-49E1-90DC-F1883F7919CD} = {8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343} + {4CE2384D-6B88-4824-ADD1-4183D180FEFF} = {8C9AA8A2-9D1F-4450-9F8D-56BAB6F3D343} + {353AA2B0-1013-486C-B5BD-9379385CA403} = {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 new file mode 100644 index 0000000000..d434fbc862 --- /dev/null +++ b/src/Middleware/RequestThrottling/RequestThrottling.slnf @@ -0,0 +1,25 @@ +{ + "solution": { + "path": "..\\Middleware.sln", + "projects": [ + "..\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", + "..\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", + "..\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", + "..\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", + "..\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", + "..\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", + "..\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", + "..\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj", + "..\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", + "..\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", + "..\\Servers\\Kestrel\\Transport.Abstractions\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj", + "..\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", + "..\\http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", + "..\\http\\http\\src\\Microsoft.AspNetCore.Http.csproj", + "HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj", + "RequestThrottling\\sample\\RequestThrottlingSample.csproj", + "RequestThrottling\\src\\Microsoft.AspNetCore.RequestThrottling.csproj", + "RequestThrottling\\test\\Microsoft.AspNetCore.RequestThrottling.Tests.csproj" + ] + } +} \ No newline at end of file diff --git a/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj new file mode 100644 index 0000000000..0a1bcdd0b9 --- /dev/null +++ b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj @@ -0,0 +1,10 @@ + + + + netcoreapp3.0 + + + + + + diff --git a/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs new file mode 100644 index 0000000000..618082bc4a --- /dev/null +++ b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs @@ -0,0 +1,3 @@ +// 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. + diff --git a/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj b/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj new file mode 100644 index 0000000000..0f80e6516a --- /dev/null +++ b/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.0 + + + + + + + + + diff --git a/src/Middleware/RequestThrottling/sample/Startup.cs b/src/Middleware/RequestThrottling/sample/Startup.cs new file mode 100644 index 0000000000..95a94be56d --- /dev/null +++ b/src/Middleware/RequestThrottling/sample/Startup.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace RequestThrottlingSample +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + app.Run(async context => + { + await context.Response.WriteAsync("Hello world!"); + }); + } + + // Entry point for the application. + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) // for the cert file + .ConfigureLogging(factory => + { + factory.SetMinimumLevel(LogLevel.Debug); + factory.AddConsole(); + }) + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj b/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj new file mode 100644 index 0000000000..5014e9cec5 --- /dev/null +++ b/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj @@ -0,0 +1,10 @@ + + + + ASP.NET Core middleware for queuing incoming HTTP requests, to avoid threadpool starvation. + netcoreapp3.0 + true + aspnetcore;queue;queuing + + + diff --git a/src/Middleware/RequestThrottling/src/Properties/AssemblyInfo.cs b/src/Middleware/RequestThrottling/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1dcaedfaa6 --- /dev/null +++ b/src/Middleware/RequestThrottling/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.RequestThrottling.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs b/src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs new file mode 100644 index 0000000000..4c79b94777 --- /dev/null +++ b/src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.RequestThrottling +{ + internal class SemaphoreWrapper : IDisposable + { + private SemaphoreSlim _semaphore; + + public SemaphoreWrapper(int queueLength) + { + _semaphore = new SemaphoreSlim(queueLength); + } + + public Task EnterQueue() + { + return _semaphore.WaitAsync(); + } + + public void LeaveQueue() + { + _semaphore.Release(); + } + + public int Count + { + get => _semaphore.CurrentCount; + } + + public void Dispose() + { + _semaphore.Dispose(); + } + } +} diff --git a/src/Middleware/RequestThrottling/test/Microsoft.AspNetCore.RequestThrottling.Tests.csproj b/src/Middleware/RequestThrottling/test/Microsoft.AspNetCore.RequestThrottling.Tests.csproj new file mode 100644 index 0000000000..8c0dd8e989 --- /dev/null +++ b/src/Middleware/RequestThrottling/test/Microsoft.AspNetCore.RequestThrottling.Tests.csproj @@ -0,0 +1,10 @@ + + + + netcoreapp3.0 + + + + + + diff --git a/src/Middleware/RequestThrottling/test/SemaphoreWrapperTests.cs b/src/Middleware/RequestThrottling/test/SemaphoreWrapperTests.cs new file mode 100644 index 0000000000..b5cdfce18f --- /dev/null +++ b/src/Middleware/RequestThrottling/test/SemaphoreWrapperTests.cs @@ -0,0 +1,66 @@ +// 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 Xunit; +using System.Threading; +using System.Threading.Tasks; +using System; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.RequestThrottling.Tests +{ + public class SemaphoreWrapperTests + { + [Fact] + public async Task TracksQueueLength() + { + using var s = new SemaphoreWrapper(1); + Assert.Equal(1, s.Count); + + await s.EnterQueue().OrTimeout(); + Assert.Equal(0, s.Count); + + s.LeaveQueue(); + Assert.Equal(1, s.Count); + } + + [Fact] + public void DoesNotWaitIfSpaceAvailible() + { + using var s = new SemaphoreWrapper(2); + + var t1 = s.EnterQueue(); + Assert.True(t1.IsCompleted); + + var t2 = s.EnterQueue(); + Assert.True(t2.IsCompleted); + + var t3 = s.EnterQueue(); + Assert.False(t3.IsCompleted); + } + + [Fact] + public async Task WaitsIfNoSpaceAvailible() + { + using var s = new SemaphoreWrapper(1); + await s.EnterQueue().OrTimeout(); + + var waitingTask = s.EnterQueue(); + Assert.False(waitingTask.IsCompleted); + + s.LeaveQueue(); + await waitingTask.OrTimeout(); + } + + [Fact] + public async Task IsEncapsulated() + { + using var s1 = new SemaphoreWrapper(1); + using var s2 = new SemaphoreWrapper(1); + + await s1.EnterQueue().OrTimeout(); + await s2.EnterQueue().OrTimeout(); + } + } +} diff --git a/src/Middleware/RequestThrottling/test/TaskExtensions.cs b/src/Middleware/RequestThrottling/test/TaskExtensions.cs new file mode 100644 index 0000000000..52ec0c4303 --- /dev/null +++ b/src/Middleware/RequestThrottling/test/TaskExtensions.cs @@ -0,0 +1,69 @@ +// 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.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Testing; + +namespace System.Threading.Tasks +{ +#if TESTUTILS + public +#else + internal +#endif + static class TaskExtensions + { + private const int DefaultTimeout = 30 * 1000; + + public static Task OrTimeout(this Task task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); + } + + public static Task OrTimeout(this Task task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return task.TimeoutAfter(timeout, filePath, lineNumber ?? 0); + } + + public static Task OrTimeout(this ValueTask task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) => + OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); + + public static Task OrTimeout(this ValueTask task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) => + task.AsTask().OrTimeout(timeout, memberName, filePath, lineNumber); + + public static Task OrTimeout(this Task task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); + } + + public static Task OrTimeout(this Task task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return task.TimeoutAfter(timeout, filePath, lineNumber ?? 0); + } + + public static async Task OrThrowIfOtherFails(this Task task, Task otherTask) + { + var completed = await Task.WhenAny(task, otherTask); + if (completed == otherTask && otherTask.IsFaulted) + { + // Manifest the exception + otherTask.GetAwaiter().GetResult(); + throw new Exception("Unreachable code"); + } + else + { + // Await the task we were asked to await. Either it's finished, or the otherTask finished successfully, and it's not our job to check that + await task; + } + } + + public static async Task OrThrowIfOtherFails(this Task task, Task otherTask) + { + await OrThrowIfOtherFails((Task)task, otherTask); + + // If we get here, 'task' is finished and succeeded. + return task.GetAwaiter().GetResult(); + } + } +} diff --git a/src/Middleware/build.cmd b/src/Middleware/build.cmd new file mode 100644 index 0000000000..033fe6f614 --- /dev/null +++ b/src/Middleware/build.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +SET RepoRoot=%~dp0..\.. +%RepoRoot%\build.cmd -projects %~dp0\**\*.*proj %*