Skip to content

Endpoint-based concurrency limiter #20835

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.ConcurrencyLimiter;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Concurrency limit extension methods for <see cref="IEndpointConventionBuilder"/>
/// </summary>
public static class ConcurrencyLimiterEndpointConventionBuilderExtensions
{
/// <summary>
/// Adds the concurrency limit with LIFO stack as queueing strategy to the endpoint(s).
/// </summary>
/// <typeparam name="TBuilder"></typeparam>
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
/// <param name="maxConcurrentRequests">
/// Maximum number of concurrent requests. Any extras will be queued on the server.
/// This option is highly application dependant, and must be configured by the application.
/// </param>
/// <param name="requestQueueLimit">
///Maximum number of queued requests before the server starts rejecting connections with '503 Service Unavailible'.
/// This option is highly application dependant, and must be configured by the application.
/// </param>
/// <returns></returns>
public static TBuilder RequireStackPolicy<TBuilder>(this TBuilder builder,
int maxConcurrentRequests,
int requestQueueLimit)
where TBuilder : IEndpointConventionBuilder
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Add(endpoints =>
{
endpoints.Metadata.Add(new StackPolicyAttribute(maxConcurrentRequests, requestQueueLimit));
});

return builder;
}
/// <summary>
/// Adds the concurrency limit with FIFO queue as queueing strategy to the endpoint(s).
/// </summary>
/// <typeparam name="TBuilder"></typeparam>
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
/// <param name="maxConcurrentRequests">
/// Maximum number of concurrent requests. Any extras will be queued on the server.
/// This option is highly application dependant, and must be configured by the application.
/// </param>
/// <param name="requestQueueLimit">
///Maximum number of queued requests before the server starts rejecting connections with '503 Service Unavailible'.
/// This option is highly application dependant, and must be configured by the application.
/// </param>
/// <returns></returns>
public static TBuilder RequireQueuePolicy<TBuilder>(this TBuilder builder,
int maxConcurrentRequests,
int requestQueueLimit)
where TBuilder : IEndpointConventionBuilder
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Add(endpoints =>
{
endpoints.Metadata.Add(new QueuePolicyAttribute(maxConcurrentRequests, requestQueueLimit));
});

return builder;
}
/// <summary>
/// Suppresses the concurrency limit to the endpoint(s).
/// </summary>
/// <typeparam name="TBuilder"></typeparam>
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
/// <returns></returns>
public static TBuilder SupressQueuePolicy<TBuilder>(this TBuilder builder)
where TBuilder : IEndpointConventionBuilder
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

builder.Add(endpoints =>
{
endpoints.Metadata.Add(new SuppressQueuePolicyAttribute());
});

return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -46,7 +47,19 @@ public ConcurrencyLimiterMiddleware(RequestDelegate next, ILoggerFactory loggerF
/// <returns>A <see cref="Task"/> that completes when the request leaves.</returns>
public async Task Invoke(HttpContext context)
{
var waitInQueueTask = _queuePolicy.TryEnterAsync();
var endpoint = context.GetEndpoint();

if (endpoint?.Metadata.GetMetadata<ISuppressQueuePolicyMetadata>() != null)
{
await _next(context);

return;
}

var queuePolicy = endpoint?.Metadata.GetMetadata<IQueuePolicy>()
?? _queuePolicy;

var waitInQueueTask = queuePolicy.TryEnterAsync();

// Make sure we only ever call GetResult once on the TryEnterAsync ValueTask b/c it resets.
bool result;
Expand All @@ -72,7 +85,7 @@ public async Task Invoke(HttpContext context)
}
finally
{
_queuePolicy.OnExit();
queuePolicy.OnExit();
}
}
else
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.ConcurrencyLimiter
{
public interface ISuppressQueuePolicyMetadata
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.DependencyInjection
public static class QueuePolicyServiceCollectionExtensions
{
/// <summary>
/// Tells <see cref="ConcurrencyLimiterMiddleware"/> to use a FIFO queue as its queueing strategy.
/// Tells <see cref="ConcurrencyLimiterMiddleware"/> to use a FIFO queue as its default queueing strategy.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
/// <param name="configure">Set the options used by the queue.
Expand All @@ -26,7 +26,7 @@ public static IServiceCollection AddQueuePolicy(this IServiceCollection services
}

/// <summary>
/// Tells <see cref="ConcurrencyLimiterMiddleware"/> to use a LIFO stack as its queueing strategy.
/// Tells <see cref="ConcurrencyLimiterMiddleware"/> to use a LIFO stack as its default queueing strategy.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
/// <param name="configure">Set the options used by the queue.
Expand Down
48 changes: 48 additions & 0 deletions src/Middleware/ConcurrencyLimiter/src/QueuePolicyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.ConcurrencyLimiter
{
/// <summary>
/// Specifies that the class or method that this attribute applied to requires limit concurrency request with FIFO queue as queueing strategy.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class QueuePolicyAttribute : Attribute, IQueuePolicy
{
private readonly QueuePolicy _queuePolicy;
/// <summary>
/// Initializes a new instance of the <see cref="QueuePolicyAttribute"/> class.
/// </summary>
/// <param name="maxConcurrentRequests">
/// Maximum number of concurrent requests. Any extras will be queued on the server.
/// This option is highly application dependant, and must be configured by the application.
/// </param>
/// <param name="requestQueueLimit">
///Maximum number of queued requests before the server starts rejecting connections with '503 Service Unavailible'.
/// This option is highly application dependant, and must be configured by the application.
/// </param>
public QueuePolicyAttribute(int maxConcurrentRequests, int requestQueueLimit)
{
_queuePolicy = new QueuePolicy(Options.Create(new QueuePolicyOptions()
{
MaxConcurrentRequests = maxConcurrentRequests,
RequestQueueLimit = requestQueueLimit
}));
}
/// <inheritdoc />
public void OnExit()
=> _queuePolicy.OnExit();


/// <inheritdoc />
public ValueTask<bool> TryEnterAsync()
=> _queuePolicy.TryEnterAsync();
}
}
48 changes: 48 additions & 0 deletions src/Middleware/ConcurrencyLimiter/src/StackPolicyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.ConcurrencyLimiter
{
/// <summary>
/// Specifies that the class or method that this attribute applied to requires limit concurrency request with LIFO stack as queueing strategy.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class StackPolicyAttribute : Attribute, IQueuePolicy
{
private readonly StackPolicy _stackPolicy;
/// <summary>
/// Initializes a new instance of the <see cref="StackPolicyAttribute"/> class.
/// </summary>
/// <param name="maxConcurrentRequests">
/// Maximum number of concurrent requests. Any extras will be queued on the server.
/// This option is highly application dependant, and must be configured by the application.
/// </param>
/// <param name="requestQueueLimit">
///Maximum number of queued requests before the server starts rejecting connections with '503 Service Unavailible'.
/// This option is highly application dependant, and must be configured by the application.
/// </param>
public StackPolicyAttribute(int maxConcurrentRequests, int requestQueueLimit)
{
_stackPolicy = new StackPolicy(Options.Create(new QueuePolicyOptions()
{
MaxConcurrentRequests = maxConcurrentRequests,
RequestQueueLimit = requestQueueLimit
}));
}

/// <inheritdoc />
public void OnExit()
=> _stackPolicy.OnExit();

/// <inheritdoc />
public ValueTask<bool> TryEnterAsync()
=> _stackPolicy.TryEnterAsync();
}
}
Original file line number Diff line number Diff line change
@@ -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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.ConcurrencyLimiter
{
/// <summary>
/// Specifies that the class or method that this attribute applied to does not limit concurrency request.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class SuppressQueuePolicyAttribute : Attribute, ISuppressQueuePolicyMetadata
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Xunit;

namespace Microsoft.AspNetCore.ConcurrencyLimiter.Tests
{
public class ConcurrencyLimiterEndpointConventionBuilderExtensionsTests
{
[Fact]
public void RequireStackPolicy_StackPolicyAttribute()
{
// Arrange
var builder = new TestEndpointConventionBuilder();

// Act
builder.RequireStackPolicy(maxConcurrentRequests: 1, requestQueueLimit: 1);

// Assert
var convention = Assert.Single(builder.Conventions);
var endpoint = new RouteEndpointBuilder(context => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0);
convention(endpoint);

Assert.IsAssignableFrom<StackPolicyAttribute>(Assert.Single(endpoint.Metadata));
}
[Fact]
public void RequireQueuePolicy_QueuePolicyAttribute()
{
// Arrange
var builder = new TestEndpointConventionBuilder();

// Act
builder.RequireQueuePolicy(maxConcurrentRequests: 1, requestQueueLimit: 1);

// Assert
var convention = Assert.Single(builder.Conventions);
var endpoint = new RouteEndpointBuilder(context => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0);
convention(endpoint);

Assert.IsAssignableFrom<QueuePolicyAttribute>(Assert.Single(endpoint.Metadata));
}
[Fact]
public void SupressQueuePolicy_ISuppressQueuePolicyMetadata()
{
// Arrange
var builder = new TestEndpointConventionBuilder();

// Act
builder.SupressQueuePolicy();

// Assert
var convention = Assert.Single(builder.Conventions);
var endpoint = new RouteEndpointBuilder(context => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0);
convention(endpoint);

Assert.IsAssignableFrom<ISuppressQueuePolicyMetadata>(Assert.Single(endpoint.Metadata));
}
private class TestEndpointConventionBuilder : IEndpointConventionBuilder
{
public IList<Action<EndpointBuilder>> Conventions { get; } = new List<Action<EndpointBuilder>>();

public void Add(Action<EndpointBuilder> convention)
{
Conventions.Add(convention);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
Expand All @@ -13,5 +13,6 @@
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.Hosting" />
<Reference Include="Microsoft.AspNetCore.ConcurrencyLimiter" />
<Reference Include="Microsoft.AspNetCore.Routing" />
</ItemGroup>
</Project>
Loading