Remove QueuePolicy locks (#23187)

* Use field rather than property

Operate on a field directly, rather than through a property.

* Make field readonly

Make field read-only as its value is never changed.

* Remove lock

Use Interlocked.Decrement() instead of taking a lock.

* Remove lock for increment

This removes the need for the lock in the success case at the cost of an extra  Interlocked.Decrement() call for the failed case.

* Fix typos

Change "availible" to "available".

* Add unit test for full queue

Add a unit test that validates request is not queued if the queue is already full.
This commit is contained in:
Martin Costello 2020-06-30 06:11:58 +01:00 committed by GitHub
parent 1c27ba1bbd
commit acabbbc61b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 33 additions and 17 deletions

View File

@ -96,7 +96,7 @@ namespace Microsoft.AspNetCore.ConcurrencyLimiter
LoggerMessage.Define<int>(LogLevel.Debug, new EventId(3, "RequestRunImmediately"), "Below MaxConcurrentRequests limit, running request immediately. Current active requests: {ActiveRequests}"); LoggerMessage.Define<int>(LogLevel.Debug, new EventId(3, "RequestRunImmediately"), "Below MaxConcurrentRequests limit, running request immediately. Current active requests: {ActiveRequests}");
private static readonly Action<ILogger, Exception> _requestRejectedQueueFull = private static readonly Action<ILogger, Exception> _requestRejectedQueueFull =
LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequestRejectedQueueFull"), "Currently at the 'RequestQueueLimit', rejecting this request with a '503 server not availible' error"); LoggerMessage.Define(LogLevel.Debug, new EventId(4, "RequestRejectedQueueFull"), "Currently at the 'RequestQueueLimit', rejecting this request with a '503 server not available' error");
internal static void RequestEnqueued(ILogger logger, int activeRequests) internal static void RequestEnqueued(ILogger logger, int activeRequests)
{ {

View File

@ -13,9 +13,9 @@ namespace Microsoft.AspNetCore.ConcurrencyLimiter
private readonly int _maxTotalRequest; private readonly int _maxTotalRequest;
private readonly SemaphoreSlim _serverSemaphore; private readonly SemaphoreSlim _serverSemaphore;
private object _totalRequestsLock = new object(); private int _totalRequests;
public int TotalRequests { get; private set; } public int TotalRequests => _totalRequests;
public QueuePolicy(IOptions<QueuePolicyOptions> options) public QueuePolicy(IOptions<QueuePolicyOptions> options)
{ {
@ -44,14 +44,12 @@ namespace Microsoft.AspNetCore.ConcurrencyLimiter
// a return value of 'true' indicates that the request may proceed // a return value of 'true' indicates that the request may proceed
// _serverSemaphore.Release is *not* called in this method, it is called externally when requests leave the server // _serverSemaphore.Release is *not* called in this method, it is called externally when requests leave the server
lock (_totalRequestsLock) int totalRequests = Interlocked.Increment(ref _totalRequests);
{
if (TotalRequests >= _maxTotalRequest)
{
return new ValueTask<bool>(false);
}
TotalRequests++; if (totalRequests > _maxTotalRequest)
{
Interlocked.Decrement(ref _totalRequests);
return new ValueTask<bool>(false);
} }
Task task = _serverSemaphore.WaitAsync(); Task task = _serverSemaphore.WaitAsync();
@ -67,10 +65,7 @@ namespace Microsoft.AspNetCore.ConcurrencyLimiter
{ {
_serverSemaphore.Release(); _serverSemaphore.Release();
lock (_totalRequestsLock) Interlocked.Decrement(ref _totalRequests);
{
TotalRequests--;
}
} }
public void Dispose() public void Dispose()

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.ConcurrencyLimiter
public int MaxConcurrentRequests { get; set; } public int MaxConcurrentRequests { get; set; }
/// <summary> /// <summary>
/// Maximum number of queued requests before the server starts rejecting connections with '503 Service Unavailible'. /// Maximum number of queued requests before the server starts rejecting connections with '503 Service Unavailable'.
/// This option is highly application dependant, and must be configured by the application. /// This option is highly application dependant, and must be configured by the application.
/// </summary> /// </summary>
public int RequestQueueLimit { get; set; } public int RequestQueueLimit { get; set; }

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.ConcurrencyLimiter.Tests.PolicyTests
public class QueuePolicyTests public class QueuePolicyTests
{ {
[Fact] [Fact]
public void DoesNotWaitIfSpaceAvailible() public void DoesNotWaitIfSpaceAvailable()
{ {
using var s = TestUtils.CreateQueuePolicy(2); using var s = TestUtils.CreateQueuePolicy(2);
@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.ConcurrencyLimiter.Tests.PolicyTests
} }
[Fact] [Fact]
public async Task WaitsIfNoSpaceAvailible() public async Task WaitsIfNoSpaceAvailable()
{ {
using var s = TestUtils.CreateQueuePolicy(1); using var s = TestUtils.CreateQueuePolicy(1);
Assert.True(await s.TryEnterAsync().OrTimeout()); Assert.True(await s.TryEnterAsync().OrTimeout());
@ -36,6 +36,27 @@ namespace Microsoft.AspNetCore.ConcurrencyLimiter.Tests.PolicyTests
Assert.True(await waitingTask.OrTimeout()); Assert.True(await waitingTask.OrTimeout());
} }
[Fact]
public void DoesNotWaitIfQueueFull()
{
using var s = TestUtils.CreateQueuePolicy(2, 1);
var t1 = s.TryEnterAsync();
Assert.True(t1.IsCompleted);
Assert.True(t1.Result);
var t2 = s.TryEnterAsync();
Assert.True(t2.IsCompleted);
Assert.True(t2.Result);
var t3 = s.TryEnterAsync();
Assert.False(t3.IsCompleted);
var t4 = s.TryEnterAsync();
Assert.True(t4.IsCompleted);
Assert.False(t4.Result);
}
[Fact] [Fact]
public async Task IsEncapsulated() public async Task IsEncapsulated()
{ {