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!
This commit is contained in:
parent
01d20c134c
commit
9969e99ef4
|
|
@ -5,6 +5,8 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
|
||||||
<Compile Include="Microsoft.AspNetCore.RequestThrottling.netcoreapp3.0.cs" />
|
<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>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,24 @@
|
||||||
// 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.
|
// 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 { } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
|
||||||
<Reference Include="Microsoft.Extensions.Logging.Console" />
|
<Reference Include="Microsoft.Extensions.Logging.Console" />
|
||||||
<Reference Include="Microsoft.AspNetCore.RequestThrottling" />
|
<Reference Include="Microsoft.AspNetCore.RequestThrottling" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
using System;
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
using System.Collections.Generic;
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.RequestThrottling;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
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
|
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
||||||
public void ConfigureServices(IServiceCollection services)
|
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 =>
|
app.Run(async context =>
|
||||||
{
|
{
|
||||||
await context.Response.WriteAsync("Hello world!");
|
await context.Response.WriteAsync("Hello Request Throttling! <p></p>");
|
||||||
|
await Task.Delay(1000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>ASP.NET Core middleware for queuing incoming HTTP requests, to avoid threadpool starvation.</Description>
|
<Description>ASP.NET Core middleware for queuing incoming HTTP requests, to avoid threadpool starvation.</Description>
|
||||||
|
|
@ -7,4 +7,10 @@
|
||||||
<PackageTags>aspnetcore;queue;queuing</PackageTags>
|
<PackageTags>aspnetcore;queue;queuing</PackageTags>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
|
||||||
|
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
|
<Reference Include="Microsoft.Extensions.Options" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Http" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Hosting" />
|
||||||
<Reference Include="Microsoft.AspNetCore.RequestThrottling" />
|
<Reference Include="Microsoft.AspNetCore.RequestThrottling" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,34 +1,48 @@
|
||||||
// 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.
|
// 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.Threading.Tasks;
|
||||||
using System;
|
using Microsoft.AspNetCore.RequestThrottling.Internal;
|
||||||
using System.Runtime.CompilerServices;
|
using Xunit;
|
||||||
using Microsoft.AspNetCore.Testing;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.RequestThrottling.Tests
|
namespace Microsoft.AspNetCore.RequestThrottling.Tests
|
||||||
{
|
{
|
||||||
public class SemaphoreWrapperTests
|
public class RequestQueueTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[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);
|
Assert.Equal(1, s.Count);
|
||||||
|
|
||||||
await s.EnterQueue().OrTimeout();
|
await s.EnterQueue().OrTimeout();
|
||||||
Assert.Equal(0, s.Count);
|
Assert.Equal(0, s.Count);
|
||||||
|
|
||||||
s.LeaveQueue();
|
s.Release();
|
||||||
Assert.Equal(1, s.Count);
|
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]
|
[Fact]
|
||||||
public void DoesNotWaitIfSpaceAvailible()
|
public void DoesNotWaitIfSpaceAvailible()
|
||||||
{
|
{
|
||||||
using var s = new SemaphoreWrapper(2);
|
using var s = new RequestQueue(2);
|
||||||
|
|
||||||
var t1 = s.EnterQueue();
|
var t1 = s.EnterQueue();
|
||||||
Assert.True(t1.IsCompleted);
|
Assert.True(t1.IsCompleted);
|
||||||
|
|
@ -43,21 +57,21 @@ namespace Microsoft.AspNetCore.RequestThrottling.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WaitsIfNoSpaceAvailible()
|
public async Task WaitsIfNoSpaceAvailible()
|
||||||
{
|
{
|
||||||
using var s = new SemaphoreWrapper(1);
|
using var s = new RequestQueue(1);
|
||||||
await s.EnterQueue().OrTimeout();
|
await s.EnterQueue().OrTimeout();
|
||||||
|
|
||||||
var waitingTask = s.EnterQueue();
|
var waitingTask = s.EnterQueue();
|
||||||
Assert.False(waitingTask.IsCompleted);
|
Assert.False(waitingTask.IsCompleted);
|
||||||
|
|
||||||
s.LeaveQueue();
|
s.Release();
|
||||||
await waitingTask.OrTimeout();
|
await waitingTask.OrTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task IsEncapsulated()
|
public async Task IsEncapsulated()
|
||||||
{
|
{
|
||||||
using var s1 = new SemaphoreWrapper(1);
|
using var s1 = new RequestQueue(1);
|
||||||
using var s2 = new SemaphoreWrapper(1);
|
using var s2 = new RequestQueue(1);
|
||||||
|
|
||||||
await s1.EnterQueue().OrTimeout();
|
await s1.EnterQueue().OrTimeout();
|
||||||
await s2.EnterQueue().OrTimeout();
|
await s2.EnterQueue().OrTimeout();
|
||||||
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>ASP.NET Core middleware for caching HTTP responses on the server.</Description>
|
<Description>ASP.NET Core middleware for caching HTTP responses on the server.</Description>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SignalR.Tests
|
namespace Microsoft.AspNetCore.Internal
|
||||||
{
|
{
|
||||||
public class SyncPoint
|
public class SyncPoint
|
||||||
{
|
{
|
||||||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Http.Connections;
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
using Microsoft.AspNetCore.Http.Connections.Client;
|
using Microsoft.AspNetCore.Http.Connections.Client;
|
||||||
using Microsoft.AspNetCore.Http.Connections.Client.Internal;
|
using Microsoft.AspNetCore.Http.Connections.Client.Internal;
|
||||||
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.SignalR.Tests;
|
using Microsoft.AspNetCore.SignalR.Tests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
|
@ -90,8 +91,8 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
var startCounter = 0;
|
var startCounter = 0;
|
||||||
var expected = new Exception("Transport failed to start");
|
var expected = new Exception("Transport failed to start");
|
||||||
|
|
||||||
// We have 4 cases here. Falling back once, falling back twice and each of these
|
// 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
|
// 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
|
// 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).
|
// because we still try to start twice (SSE and LP).
|
||||||
if (!TestHelpers.IsWebSocketsSupported() && passThreshold > 2)
|
if (!TestHelpers.IsWebSocketsSupported() && passThreshold > 2)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
using Microsoft.AspNetCore.SignalR.Tests;
|
using Microsoft.AspNetCore.SignalR.Tests;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
<Compile Include="$(SignalRSharedSourceRoot)MemoryBufferWriter.cs" Link="MemoryBufferWriter.cs" />
|
<Compile Include="$(SignalRSharedSourceRoot)MemoryBufferWriter.cs" Link="MemoryBufferWriter.cs" />
|
||||||
<Compile Include="$(SignalRSharedSourceRoot)TextMessageFormatter.cs" Link="TextMessageFormatter.cs" />
|
<Compile Include="$(SignalRSharedSourceRoot)TextMessageFormatter.cs" Link="TextMessageFormatter.cs" />
|
||||||
<Compile Include="$(SignalRSharedSourceRoot)TextMessageParser.cs" Link="TextMessageParser.cs" />
|
<Compile Include="$(SignalRSharedSourceRoot)TextMessageParser.cs" Link="TextMessageParser.cs" />
|
||||||
|
<Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Http.Connections.Client.Internal;
|
using Microsoft.AspNetCore.Http.Connections.Client.Internal;
|
||||||
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.SignalR.Tests;
|
using Microsoft.AspNetCore.SignalR.Tests;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Http.Connections.Internal;
|
||||||
using Microsoft.AspNetCore.Http.Connections.Internal.Transports;
|
using Microsoft.AspNetCore.Http.Connections.Internal.Transports;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Http.Internal;
|
using Microsoft.AspNetCore.Http.Internal;
|
||||||
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.SignalR.Tests;
|
using Microsoft.AspNetCore.SignalR.Tests;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="$(SharedSourceRoot)Buffers.Testing\**\*.cs" />
|
<Compile Include="$(SharedSourceRoot)Buffers.Testing\**\*.cs" />
|
||||||
|
<Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="$(SharedSourceRoot)SyncPoint\SyncPoint.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="$(SignalRTestUtilsProject)" />
|
<ProjectReference Include="$(SignalRTestUtilsProject)" />
|
||||||
|
|
@ -23,4 +26,4 @@
|
||||||
<Reference Include="System.Reactive.Linq" />
|
<Reference Include="System.Reactive.Linq" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.SignalR.Protocol;
|
using Microsoft.AspNetCore.SignalR.Protocol;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue