From 9969e99ef4170085dae20aab3785d8d15e510dfd Mon Sep 17 00:00:00 2001
From: Dylan Dmitri Gray <d.dylan.g@gmail.com>
Date: Mon, 27 May 2019 14:46:14 -0700
Subject: [PATCH] Dylan/request throttle (#10413)

* request throttling -- initial implementation

* prevented semaphore leak; added xml docs

* small doc fixes

* reference document

* Added internals folder, added structured logging,

* removed typo'd dependency

* no default MaxConcurrentRequests; other polishing

* renamed SemaphoreWrapper->RequestQueue; cleanup

* moved SyncPoint; prevented possible semaphore leak

* adjusting feedback

* regen refs

* Final changes!
---
 ...rosoft.AspNetCore.RequestThrottling.csproj |   4 +-
 ...NetCore.RequestThrottling.netcoreapp3.0.cs |  21 ++++
 .../sample/RequestThrottlingSample.csproj     |   4 +-
 .../RequestThrottling/sample/Startup.cs       |  19 ++-
 .../src/Internal/RequestQueue.cs              |  64 ++++++++++
 ...rosoft.AspNetCore.RequestThrottling.csproj |   8 +-
 .../src/RequestThrottlingExtensions.cs        |  29 +++++
 .../src/RequestThrottlingMiddleware.cs        | 116 ++++++++++++++++++
 .../src/RequestThrottlingOptions.cs           |  19 +++
 .../RequestThrottling/src/SemaphoreWrapper.cs |  38 ------
 ....AspNetCore.RequestThrottling.Tests.csproj |   8 +-
 .../RequestThrottling/test/MiddlewareTests.cs |  86 +++++++++++++
 ...reWrapperTests.cs => RequestQueueTests.cs} |  42 ++++---
 .../RequestThrottling/test/TestUtils.cs       |  28 +++++
 .../samples/ResponseCachingSample/Startup.cs  |   2 +-
 ...icrosoft.AspNetCore.ResponseCaching.csproj |   2 +-
 .../Microsoft.AspNetCore.ANCMSymbols.csproj   |  10 ++
 ...ft.AspNetCore.ANCMSymbols.netcoreapp3.0.cs |   3 +
 .../SyncPoint}/SyncPoint.cs                   |   2 +-
 ...HttpConnectionTests.ConnectionLifecycle.cs |   5 +-
 .../HubConnectionTests.ConnectionLifecycle.cs |   1 +
 ...oft.AspNetCore.SignalR.Client.Tests.csproj |   3 +-
 .../ServerSentEventsTransportTests.cs         |   1 +
 .../test/HttpConnectionDispatcherTests.cs     |   1 +
 ...t.AspNetCore.Http.Connections.Tests.csproj |   3 +-
 .../Microsoft.AspNetCore.SignalR.Tests.csproj |   7 +-
 .../SignalR/test/SerializedHubMessageTests.cs |   1 +
 27 files changed, 455 insertions(+), 72 deletions(-)
 create mode 100644 src/Middleware/RequestThrottling/src/Internal/RequestQueue.cs
 create mode 100644 src/Middleware/RequestThrottling/src/RequestThrottlingExtensions.cs
 create mode 100644 src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs
 create mode 100644 src/Middleware/RequestThrottling/src/RequestThrottlingOptions.cs
 delete mode 100644 src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs
 create mode 100644 src/Middleware/RequestThrottling/test/MiddlewareTests.cs
 rename src/Middleware/RequestThrottling/test/{SemaphoreWrapperTests.cs => RequestQueueTests.cs} (60%)
 create mode 100644 src/Middleware/RequestThrottling/test/TestUtils.cs
 create mode 100644 src/Servers/IIS/AspNetCoreModuleV2/ref/Microsoft.AspNetCore.ANCMSymbols.csproj
 create mode 100644 src/Servers/IIS/AspNetCoreModuleV2/ref/Microsoft.AspNetCore.ANCMSymbols.netcoreapp3.0.cs
 rename src/{SignalR/common/testassets/Tests.Utils => Shared/SyncPoint}/SyncPoint.cs (98%)

diff --git a/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj
index 0a1bcdd0b91..8e8c1dfce6c 100644
--- a/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj
+++ b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.csproj
@@ -5,6 +5,8 @@
   </PropertyGroup>
   <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
     <Compile Include="Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs" />
-    
+    <Reference Include="Microsoft.AspNetCore.Http.Abstractions"  />
+    <Reference Include="Microsoft.Extensions.Logging.Abstractions"  />
+    <Reference Include="Microsoft.Extensions.Options"  />
   </ItemGroup>
 </Project>
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 618082bc4a8..b5a3acf406a 100644
--- a/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs
+++ b/src/Middleware/RequestThrottling/ref/Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs
@@ -1,3 +1,24 @@
 // 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.
 
+namespace Microsoft.AspNetCore.Builder
+{
+    public static partial class RequestThrottlingExtensions
+    {
+        public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseRequestThrottling(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { throw null; }
+    }
+}
+namespace Microsoft.AspNetCore.RequestThrottling
+{
+    public partial class RequestThrottlingMiddleware
+    {
+        public RequestThrottlingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.RequestThrottling.RequestThrottlingOptions> options) { }
+        [System.Diagnostics.DebuggerStepThroughAttribute]
+        public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext context) { throw null; }
+    }
+    public partial class RequestThrottlingOptions
+    {
+        public RequestThrottlingOptions() { }
+        public int? MaxConcurrentRequests { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+    }
+}
diff --git a/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj b/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj
index 0f80e6516ad..9f49f115c0e 100644
--- a/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj
+++ b/src/Middleware/RequestThrottling/sample/RequestThrottlingSample.csproj
@@ -1,13 +1,13 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
+<Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
     <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
-    <Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
     <Reference Include="Microsoft.Extensions.Logging.Console" />
     <Reference Include="Microsoft.AspNetCore.RequestThrottling" />
+    <Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
   </ItemGroup>
 
 </Project>
diff --git a/src/Middleware/RequestThrottling/sample/Startup.cs b/src/Middleware/RequestThrottling/sample/Startup.cs
index 95a94be56d6..113f48d8604 100644
--- a/src/Middleware/RequestThrottling/sample/Startup.cs
+++ b/src/Middleware/RequestThrottling/sample/Startup.cs
@@ -1,12 +1,12 @@
-using System;
-using System.Collections.Generic;
+// 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.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.AspNetCore.RequestThrottling;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
@@ -19,13 +19,20 @@ namespace RequestThrottlingSample
         // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
         public void ConfigureServices(IServiceCollection services)
         {
+            services.Configure<RequestThrottlingOptions>(options =>
+            {
+                options.MaxConcurrentRequests = 2;
+            });
         }
 
-        public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
+        public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
         {
+            app.UseRequestThrottling();
+
             app.Run(async context =>
             {
-                await context.Response.WriteAsync("Hello world!");
+                await context.Response.WriteAsync("Hello Request Throttling! <p></p>");
+                await Task.Delay(1000);
             });
         }
 
diff --git a/src/Middleware/RequestThrottling/src/Internal/RequestQueue.cs b/src/Middleware/RequestThrottling/src/Internal/RequestQueue.cs
new file mode 100644
index 00000000000..a09ddeb79dd
--- /dev/null
+++ b/src/Middleware/RequestThrottling/src/Internal/RequestQueue.cs
@@ -0,0 +1,64 @@
+// 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;
+
+namespace Microsoft.AspNetCore.RequestThrottling.Internal
+{
+    internal class RequestQueue : IDisposable
+    {
+        private SemaphoreSlim _semaphore;
+        private object _waitingRequestsLock = new object();
+        public readonly int MaxConcurrentRequests;
+        public int WaitingRequests { get; private set; }
+
+        public RequestQueue(int maxConcurrentRequests)
+        {
+            MaxConcurrentRequests = maxConcurrentRequests;
+            _semaphore = new SemaphoreSlim(maxConcurrentRequests);
+        }
+
+        public async Task EnterQueue()
+        {
+            var waitInQueueTask = _semaphore.WaitAsync();
+
+            var needsToWaitOnQueue = !waitInQueueTask.IsCompletedSuccessfully;
+            if (needsToWaitOnQueue)
+            {
+                lock (_waitingRequestsLock)
+                {
+                    WaitingRequests++;
+                }
+
+                await waitInQueueTask;
+
+                lock (_waitingRequestsLock)
+                {
+                    WaitingRequests--;
+                }
+            }
+        }
+
+        public void Release()
+        {
+            _semaphore.Release();
+        }
+
+        public int Count
+        {
+            get => _semaphore.CurrentCount;
+        }
+
+        public int ConcurrentRequests
+        {
+            get => MaxConcurrentRequests - _semaphore.CurrentCount;
+        }
+
+        public void Dispose()
+        {
+            _semaphore.Dispose();
+        }
+    }
+}
diff --git a/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj b/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj
index 5014e9cec59..0090f373c0f 100644
--- a/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj
+++ b/src/Middleware/RequestThrottling/src/Microsoft.AspNetCore.RequestThrottling.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <Description>ASP.NET Core middleware for queuing incoming HTTP requests, to avoid threadpool starvation.</Description>
@@ -7,4 +7,10 @@
     <PackageTags>aspnetcore;queue;queuing</PackageTags>
   </PropertyGroup>
 
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
+    <Reference Include="Microsoft.Extensions.Logging.Abstractions" />
+    <Reference Include="Microsoft.Extensions.Options" />
+  </ItemGroup>
+
 </Project>
diff --git a/src/Middleware/RequestThrottling/src/RequestThrottlingExtensions.cs b/src/Middleware/RequestThrottling/src/RequestThrottlingExtensions.cs
new file mode 100644
index 00000000000..17968b44d59
--- /dev/null
+++ b/src/Middleware/RequestThrottling/src/RequestThrottlingExtensions.cs
@@ -0,0 +1,29 @@
+// 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 Microsoft.AspNetCore.RequestThrottling;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    /// <summary>
+    /// Extension methods for adding the <see cref="RequestThrottlingMiddleware"/> to an application.
+    /// </summary>
+    public static class RequestThrottlingExtensions
+    {
+        /// <summary>
+        /// Adds the <see cref="RequestThrottlingMiddleware"/> to limit the number of concurrently-executing requests.
+        /// </summary>
+        /// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
+        /// <returns>The <see cref="IApplicationBuilder"/>.</returns>
+        public static IApplicationBuilder UseRequestThrottling(this IApplicationBuilder app)
+        {
+            if (app == null)
+            {
+                throw new ArgumentNullException(nameof(app));
+            }
+
+            return app.UseMiddleware<RequestThrottlingMiddleware>();
+        }
+    }
+}
diff --git a/src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs b/src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs
new file mode 100644
index 00000000000..9eaec8cfbc3
--- /dev/null
+++ b/src/Middleware/RequestThrottling/src/RequestThrottlingMiddleware.cs
@@ -0,0 +1,116 @@
+// 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;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.RequestThrottling.Internal;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.RequestThrottling
+{
+    /// <summary>
+    /// Limits the number of concurrent requests allowed in the application.
+    /// </summary>
+    public class RequestThrottlingMiddleware
+    {
+        private readonly RequestQueue _requestQueue;
+        private readonly RequestThrottlingOptions _options;
+        private readonly RequestDelegate _next;
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// Creates a new <see cref="RequestThrottlingMiddleware"/>.
+        /// </summary>
+        /// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param>
+        /// <param name="loggerFactory">The <see cref="ILoggerFactory"/> used for logging.</param>
+        /// <param name="options">The <see cref="RequestThrottlingOptions"/> containing the initialization parameters.</param>
+        public RequestThrottlingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<RequestThrottlingOptions> options)
+        {
+            if (options.Value.MaxConcurrentRequests == null)
+            {
+                throw new ArgumentException("The value of 'options.MaxConcurrentRequests' must be specified.", nameof(options));
+            }
+
+            _next = next;
+            _logger = loggerFactory.CreateLogger<RequestThrottlingMiddleware>();
+            _options = options.Value;
+            _requestQueue = new RequestQueue(_options.MaxConcurrentRequests.Value);
+        }
+
+        /// <summary>
+        /// Invokes the logic of the middleware.
+        /// </summary>
+        /// <param name="context">The <see cref="HttpContext"/>.</param>
+        /// <returns>A <see cref="Task"/> that completes when the request leaves.</returns>
+        public async Task Invoke(HttpContext context)
+        {
+            var waitInQueueTask = _requestQueue.EnterQueue();
+
+            if (waitInQueueTask.IsCompletedSuccessfully)
+            {
+                RequestThrottlingLog.RequestRunImmediately(_logger);
+            }
+            else
+            {
+                RequestThrottlingLog.RequestEnqueued(_logger, WaitingRequests);
+                await waitInQueueTask;
+                RequestThrottlingLog.RequestDequeued(_logger, WaitingRequests);
+            }
+
+            try
+            {
+                await _next(context);
+            }
+            finally
+            {
+                _requestQueue.Release();
+            }
+        }
+
+        /// <summary>
+        /// The number of live requests that are downstream from this middleware.
+        /// Cannot exceeed <see cref="RequestThrottlingOptions.MaxConcurrentRequests"/>.
+        /// </summary>
+        internal int ConcurrentRequests
+        {
+            get => _requestQueue.ConcurrentRequests;
+        }
+
+        /// <summary>
+        /// Number of requests currently enqueued and waiting to be processed.
+        /// </summary>
+        internal int WaitingRequests
+        {
+            get => _requestQueue.WaitingRequests;
+        }
+
+        private static class RequestThrottlingLog
+        {
+            private static readonly Action<ILogger, int, Exception> _requestEnqueued =
+                LoggerMessage.Define<int>(LogLevel.Debug, new EventId(1, "RequestEnqueued"), "Concurrent request limit reached, queuing request. Current queue length: {QueuedRequests}.");
+
+            private static readonly Action<ILogger, int, Exception> _requestDequeued =
+                LoggerMessage.Define<int>(LogLevel.Debug, new EventId(2, "RequestDequeued"), "Request dequeued. Current queue length: {QueuedRequests}.");
+
+            private static readonly Action<ILogger, Exception> _requestRunImmediately =
+                LoggerMessage.Define(LogLevel.Debug, new EventId(3, "RequestRunImmediately"), "Concurrent request limit has not been reached, running request immediately.");
+
+            internal static void RequestEnqueued(ILogger logger, int queuedRequests)
+            {
+                _requestEnqueued(logger, queuedRequests, null);
+            }
+
+            internal static void RequestDequeued(ILogger logger, int queuedRequests)
+            {
+                _requestDequeued(logger, queuedRequests, null);
+            }
+
+            internal static void RequestRunImmediately(ILogger logger)
+            {
+                _requestRunImmediately(logger, null);
+            }
+        }
+    }
+}
diff --git a/src/Middleware/RequestThrottling/src/RequestThrottlingOptions.cs b/src/Middleware/RequestThrottling/src/RequestThrottlingOptions.cs
new file mode 100644
index 00000000000..64a832640fa
--- /dev/null
+++ b/src/Middleware/RequestThrottling/src/RequestThrottlingOptions.cs
@@ -0,0 +1,19 @@
+// 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 Microsoft.AspNetCore.RequestThrottling;
+
+namespace Microsoft.AspNetCore.RequestThrottling
+{
+    /// <summary>
+    /// Specifies options for the <see cref="RequestThrottlingMiddleware"/>.
+    /// </summary>
+    public class RequestThrottlingOptions
+    {
+        /// <summary>
+        /// Maximum number of concurrent requests. Any extras will be queued on the server. 
+        /// This is null by default because the correct value is application specific. This option must be configured by the application.
+        /// </summary>
+        public int? MaxConcurrentRequests { get; set; }
+    }
+}
diff --git a/src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs b/src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs
deleted file mode 100644
index 4c79b947775..00000000000
--- a/src/Middleware/RequestThrottling/src/SemaphoreWrapper.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-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
index 8c0dd8e989a..78b1c886926 100644
--- a/src/Middleware/RequestThrottling/test/Microsoft.AspNetCore.RequestThrottling.Tests.csproj
+++ b/src/Middleware/RequestThrottling/test/Microsoft.AspNetCore.RequestThrottling.Tests.csproj
@@ -1,10 +1,16 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
+    <Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Http" />
+    <Reference Include="Microsoft.AspNetCore.Hosting" />
     <Reference Include="Microsoft.AspNetCore.RequestThrottling" />
   </ItemGroup>
 </Project>
diff --git a/src/Middleware/RequestThrottling/test/MiddlewareTests.cs b/src/Middleware/RequestThrottling/test/MiddlewareTests.cs
new file mode 100644
index 00000000000..8124cddb491
--- /dev/null
+++ b/src/Middleware/RequestThrottling/test/MiddlewareTests.cs
@@ -0,0 +1,86 @@
+// 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;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Internal;
+using Xunit;
+
+namespace Microsoft.AspNetCore.RequestThrottling.Tests
+{
+    public class MiddlewareTests
+    {
+        [Fact]
+        public async Task RequestsCanEnterIfSpaceAvailible()
+        {
+            var middleware = TestUtils.CreateTestMiddleWare(maxConcurrentRequests: 1);
+            var context = new DefaultHttpContext();
+
+            // a request should go through with no problems
+            await middleware.Invoke(context).OrTimeout();
+        }
+
+        [Fact]
+        public async Task SemaphoreStatePreservedIfRequestsError()
+        {
+            var middleware = TestUtils.CreateTestMiddleWare(
+                maxConcurrentRequests: 1,
+                next: httpContext =>
+                {
+                    throw new DivideByZeroException();
+                });
+
+            Assert.Equal(0, middleware.ConcurrentRequests);
+
+            await Assert.ThrowsAsync<DivideByZeroException>(() => middleware.Invoke(new DefaultHttpContext()));
+
+            Assert.Equal(0, middleware.ConcurrentRequests);
+        }
+
+        [Fact]
+        public async Task QueuedRequestsContinueWhenSpaceBecomesAvailible()
+        {
+            var blocker = new SyncPoint();
+            var firstRequest = true;
+
+            var middleware = TestUtils.CreateTestMiddleWare(
+                maxConcurrentRequests: 1,
+                next: httpContext =>
+                {
+                    if (firstRequest)
+                    {
+                        firstRequest = false;
+                        return blocker.WaitToContinue();
+                    }
+                    return Task.CompletedTask;
+                });
+
+            // t1 (as the first request) is blocked by the tcs blocker
+            var t1 = middleware.Invoke(new DefaultHttpContext());
+            Assert.Equal(1, middleware.ConcurrentRequests);
+            Assert.Equal(0, middleware.WaitingRequests);
+
+            // t2 is blocked from entering the server since t1 already exists there
+            // note: increasing MaxConcurrentRequests would allow t2 through while t1 is blocked
+            var t2 = middleware.Invoke(new DefaultHttpContext());
+            Assert.Equal(1, middleware.ConcurrentRequests);
+            Assert.Equal(1, middleware.WaitingRequests);
+
+            // unblock the first task, and the second should follow
+            blocker.Continue();
+            await t1.OrTimeout();
+            await t2.OrTimeout();
+        }
+
+        [Fact]
+        public void InvalidArgumentIfMaxConcurrentRequestsIsNull()
+        {
+            var ex = Assert.Throws<ArgumentException>(() =>
+            {
+                TestUtils.CreateTestMiddleWare(maxConcurrentRequests: null);
+            });
+            Assert.Equal("options", ex.ParamName);
+        }
+    }
+}
diff --git a/src/Middleware/RequestThrottling/test/SemaphoreWrapperTests.cs b/src/Middleware/RequestThrottling/test/RequestQueueTests.cs
similarity index 60%
rename from src/Middleware/RequestThrottling/test/SemaphoreWrapperTests.cs
rename to src/Middleware/RequestThrottling/test/RequestQueueTests.cs
index b5cdfce18f2..67eeda5d67a 100644
--- a/src/Middleware/RequestThrottling/test/SemaphoreWrapperTests.cs
+++ b/src/Middleware/RequestThrottling/test/RequestQueueTests.cs
@@ -1,34 +1,48 @@
 // 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;
+using Microsoft.AspNetCore.RequestThrottling.Internal;
+using Xunit;
 
 namespace Microsoft.AspNetCore.RequestThrottling.Tests
 {
-    public class SemaphoreWrapperTests
+    public class RequestQueueTests
     {
         [Fact]
-        public async Task TracksQueueLength()
+        public async Task LimitsIncomingRequests()
         {
-            using var s = new SemaphoreWrapper(1);
+            using var s = new RequestQueue(1);
             Assert.Equal(1, s.Count);
 
             await s.EnterQueue().OrTimeout();
             Assert.Equal(0, s.Count);
 
-            s.LeaveQueue();
+            s.Release();
             Assert.Equal(1, s.Count);
         }
 
+        [Fact]
+        public async Task TracksQueueLength()
+        {
+            using var s = new RequestQueue(1);
+            Assert.Equal(0, s.WaitingRequests);
+
+            await s.EnterQueue();
+            Assert.Equal(0, s.WaitingRequests);
+
+            var enterQueueTask = s.EnterQueue();
+            Assert.Equal(1, s.WaitingRequests);
+
+            s.Release();
+            await enterQueueTask;
+            Assert.Equal(0, s.WaitingRequests);
+        }
+
         [Fact]
         public void DoesNotWaitIfSpaceAvailible()
         {
-            using var s = new SemaphoreWrapper(2);
+            using var s = new RequestQueue(2);
 
             var t1 = s.EnterQueue();
             Assert.True(t1.IsCompleted);
@@ -43,21 +57,21 @@ namespace Microsoft.AspNetCore.RequestThrottling.Tests
         [Fact]
         public async Task WaitsIfNoSpaceAvailible()
         {
-            using var s = new SemaphoreWrapper(1);
+            using var s = new RequestQueue(1);
             await s.EnterQueue().OrTimeout();
 
             var waitingTask = s.EnterQueue();
             Assert.False(waitingTask.IsCompleted);
 
-            s.LeaveQueue();
+            s.Release();
             await waitingTask.OrTimeout();
         }
 
         [Fact]
         public async Task IsEncapsulated()
         {
-            using var s1 = new SemaphoreWrapper(1);
-            using var s2 = new SemaphoreWrapper(1);
+            using var s1 = new RequestQueue(1);
+            using var s2 = new RequestQueue(1);
 
             await s1.EnterQueue().OrTimeout();
             await s2.EnterQueue().OrTimeout();
diff --git a/src/Middleware/RequestThrottling/test/TestUtils.cs b/src/Middleware/RequestThrottling/test/TestUtils.cs
new file mode 100644
index 00000000000..438d08ab8d8
--- /dev/null
+++ b/src/Middleware/RequestThrottling/test/TestUtils.cs
@@ -0,0 +1,28 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.RequestThrottling;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.RequestThrottling.Tests
+{
+    public static class TestUtils
+    {
+        public static RequestThrottlingMiddleware CreateTestMiddleWare(int? maxConcurrentRequests, RequestDelegate next = null)
+        {
+            var options = new RequestThrottlingOptions
+            {
+                MaxConcurrentRequests = maxConcurrentRequests
+            };
+
+            return new RequestThrottlingMiddleware(
+                    next: next ?? (context => Task.CompletedTask),
+                    loggerFactory: NullLoggerFactory.Instance,
+                    options: Options.Create(options)
+                );
+        }
+    }
+}
diff --git a/src/Middleware/ResponseCaching/samples/ResponseCachingSample/Startup.cs b/src/Middleware/ResponseCaching/samples/ResponseCachingSample/Startup.cs
index ca2e7fbcf3a..6184a36946c 100644
--- a/src/Middleware/ResponseCaching/samples/ResponseCachingSample/Startup.cs
+++ b/src/Middleware/ResponseCaching/samples/ResponseCachingSample/Startup.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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;
diff --git a/src/Middleware/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Middleware/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.csproj
index 9611dfbdaf3..ef8199808ca 100644
--- a/src/Middleware/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.csproj
+++ b/src/Middleware/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <Description>ASP.NET Core middleware for caching HTTP responses on the server.</Description>
diff --git a/src/Servers/IIS/AspNetCoreModuleV2/ref/Microsoft.AspNetCore.ANCMSymbols.csproj b/src/Servers/IIS/AspNetCoreModuleV2/ref/Microsoft.AspNetCore.ANCMSymbols.csproj
new file mode 100644
index 00000000000..36c3a47a9af
--- /dev/null
+++ b/src/Servers/IIS/AspNetCoreModuleV2/ref/Microsoft.AspNetCore.ANCMSymbols.csproj
@@ -0,0 +1,10 @@
+<!-- This file is automatically generated. -->
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFrameworks>netcoreapp3.0</TargetFrameworks>
+  </PropertyGroup>
+  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
+    <Compile Include="Microsoft.AspNetCore.ANCMSymbols.netcoreapp3.0.cs" />
+    
+  </ItemGroup>
+</Project>
diff --git a/src/Servers/IIS/AspNetCoreModuleV2/ref/Microsoft.AspNetCore.ANCMSymbols.netcoreapp3.0.cs b/src/Servers/IIS/AspNetCoreModuleV2/ref/Microsoft.AspNetCore.ANCMSymbols.netcoreapp3.0.cs
new file mode 100644
index 00000000000..618082bc4a8
--- /dev/null
+++ b/src/Servers/IIS/AspNetCoreModuleV2/ref/Microsoft.AspNetCore.ANCMSymbols.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/SignalR/common/testassets/Tests.Utils/SyncPoint.cs b/src/Shared/SyncPoint/SyncPoint.cs
similarity index 98%
rename from src/SignalR/common/testassets/Tests.Utils/SyncPoint.cs
rename to src/Shared/SyncPoint/SyncPoint.cs
index 55f4a034d50..ccf36d59dfc 100644
--- a/src/SignalR/common/testassets/Tests.Utils/SyncPoint.cs
+++ b/src/Shared/SyncPoint/SyncPoint.cs
@@ -4,7 +4,7 @@
 using System;
 using System.Threading.Tasks;
 
-namespace Microsoft.AspNetCore.SignalR.Tests
+namespace Microsoft.AspNetCore.Internal
 {
     public class SyncPoint
     {
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs
index 2088ef39270..61821226b2c 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs
@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Http.Connections;
 using Microsoft.AspNetCore.Http.Connections.Client;
 using Microsoft.AspNetCore.Http.Connections.Client.Internal;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.SignalR.Tests;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Testing;
@@ -90,8 +91,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
                     var startCounter = 0;
                     var expected = new Exception("Transport failed to start");
 
-                    // We have 4 cases here. Falling back once, falling back twice and each of these 
-                    // with WebSockets available and not. If Websockets aren't available and 
+                    // We have 4 cases here. Falling back once, falling back twice and each of these
+                    // with WebSockets available and not. If Websockets aren't available and
                     // we can't to test the fallback once scenario we don't decrement the passthreshold
                     // because we still try to start twice (SSE and LP).
                     if (!TestHelpers.IsWebSocketsSupported() && passThreshold > 2)
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs
index f48742474bc..647aa433cac 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs
@@ -8,6 +8,7 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.SignalR.Protocol;
 using Microsoft.AspNetCore.SignalR.Tests;
 using Microsoft.Extensions.DependencyInjection;
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj b/src/SignalR/clients/csharp/Client/test/UnitTests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj
index a2b40fbe256..cc45556edd0 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/Microsoft.AspNetCore.SignalR.Client.Tests.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>netcoreapp3.0</TargetFramework>
@@ -8,6 +8,7 @@
     <Compile Include="$(SignalRSharedSourceRoot)MemoryBufferWriter.cs" Link="MemoryBufferWriter.cs" />
     <Compile Include="$(SignalRSharedSourceRoot)TextMessageFormatter.cs" Link="TextMessageFormatter.cs" />
     <Compile Include="$(SignalRSharedSourceRoot)TextMessageParser.cs" Link="TextMessageParser.cs" />
+    <Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/ServerSentEventsTransportTests.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/ServerSentEventsTransportTests.cs
index 421e1cef616..aa3c41bbed1 100644
--- a/src/SignalR/clients/csharp/Client/test/UnitTests/ServerSentEventsTransportTests.cs
+++ b/src/SignalR/clients/csharp/Client/test/UnitTests/ServerSentEventsTransportTests.cs
@@ -11,6 +11,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Http.Connections.Client.Internal;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.SignalR.Tests;
 using Microsoft.Extensions.Logging.Testing;
 using Moq;
diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs
index 5b4c164ce38..2fcf129790e 100644
--- a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs
+++ b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs
@@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Http.Connections.Internal;
 using Microsoft.AspNetCore.Http.Connections.Internal.Transports;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Http.Internal;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.SignalR.Tests;
 using Microsoft.AspNetCore.Testing;
 using Microsoft.AspNetCore.Testing.xunit;
diff --git a/src/SignalR/common/Http.Connections/test/Microsoft.AspNetCore.Http.Connections.Tests.csproj b/src/SignalR/common/Http.Connections/test/Microsoft.AspNetCore.Http.Connections.Tests.csproj
index 6c03dfe4ebc..1f6b36a8bae 100644
--- a/src/SignalR/common/Http.Connections/test/Microsoft.AspNetCore.Http.Connections.Tests.csproj
+++ b/src/SignalR/common/Http.Connections/test/Microsoft.AspNetCore.Http.Connections.Tests.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>netcoreapp3.0</TargetFramework>
@@ -6,6 +6,7 @@
 
   <ItemGroup>
     <Compile Include="$(SharedSourceRoot)Buffers.Testing\**\*.cs" />
+    <Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj
index c4fd5fd37b3..24958a00b6c 100644
--- a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj
+++ b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj
@@ -1,9 +1,12 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>
 
+  <ItemGroup>
+    <Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
+  </ItemGroup>
 
   <ItemGroup>
     <ProjectReference Include="$(SignalRTestUtilsProject)" />
@@ -23,4 +26,4 @@
     <Reference Include="System.Reactive.Linq" />
   </ItemGroup>
 
-</Project>
+</Project>
\ No newline at end of file
diff --git a/src/SignalR/server/SignalR/test/SerializedHubMessageTests.cs b/src/SignalR/server/SignalR/test/SerializedHubMessageTests.cs
index b69952b523b..da55a5e6751 100644
--- a/src/SignalR/server/SignalR/test/SerializedHubMessageTests.cs
+++ b/src/SignalR/server/SignalR/test/SerializedHubMessageTests.cs
@@ -1,5 +1,6 @@
 using System.Linq;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Internal;
 using Microsoft.AspNetCore.SignalR.Protocol;
 using Xunit;
 
-- 
GitLab