Skip to content
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

Multiple RateLimiter per Endpoint #4

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
27 changes: 27 additions & 0 deletions benchmark/ThrottlR.Benchmark/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:7160/",
"sslPort": 44355
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"ThrottlR.Benchmark": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}
34 changes: 34 additions & 0 deletions src/Playground/Middleware/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Threading.RateLimiting;
using Microsoft.Extensions.Options;

namespace Playground
{
public static class Extensions
{
public static TBuilder RequireRateLimit<TBuilder>(this TBuilder builder, params string[] policies) where TBuilder : IEndpointConventionBuilder
{
foreach (var policy in policies)
{
builder.WithMetadata(new RateLimitPolicy(policy));
}

return builder;
}

public static TBuilder RequireRateLimit<TBuilder>(this TBuilder builder, PartitionedRateLimiter<HttpContext> rateLimiter) where TBuilder : IEndpointConventionBuilder
{
return builder.WithMetadata(new RateLimitInline(rateLimiter));
}

public static TBuilder RequireTokenBucketRateLimit<TBuilder>(this TBuilder builder, int tokenlimit, TimeSpan replenishmentPeriod, int tokensPerPeriod) where TBuilder : IEndpointConventionBuilder
{
return builder.WithMetadata(new TokenBucketRateLimitInline(tokenlimit, replenishmentPeriod, tokensPerPeriod));
}

public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder app, PartitionedOptions options)
{
return app.UseMiddleware<RateLimitMiddleware>(Options.Create<PartitionedOptions>(options));
}
}

}
8 changes: 8 additions & 0 deletions src/Playground/Middleware/IDisableRateLimitMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Playground
{
public interface IDisableRateLimitMetadata
{

}

}
10 changes: 10 additions & 0 deletions src/Playground/Middleware/IRateLimitInline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.RateLimiting;

namespace Playground
{
public interface IRateLimitInline : IRateLimitMetadata
{
PartitionedRateLimiter<HttpContext> RateLimiter { get; }
}

}
8 changes: 8 additions & 0 deletions src/Playground/Middleware/IRateLimitMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Playground
{
public interface IRateLimitMetadata
{

}

}
8 changes: 8 additions & 0 deletions src/Playground/Middleware/IRateLimitPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Playground
{
public interface IRateLimitPolicy : IRateLimitMetadata
{
string PolicyName { get; }
}

}
16 changes: 16 additions & 0 deletions src/Playground/Middleware/PartitionedOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Threading.RateLimiting;

namespace Playground
{
public class PartitionedOptions
{
public Dictionary<string, PartitionedRateLimiter<HttpContext>> Policies { get; } = new();

public void AddPolicy<TPartitionKey>(string policyName, Func<HttpContext, RateLimitPartition<TPartitionKey>> partitioner, IEqualityComparer<TPartitionKey>? equalityComparer = null)
where TPartitionKey : notnull
{
Policies.Add(policyName, PartitionedRateLimiter.Create(partitioner, equalityComparer));
}
}

}
15 changes: 15 additions & 0 deletions src/Playground/Middleware/RateLimitInline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Threading.RateLimiting;

namespace Playground
{
public class RateLimitInline : IRateLimitInline
{
public RateLimitInline(PartitionedRateLimiter<HttpContext> rateLimiter)
{
RateLimiter = rateLimiter;
}

public PartitionedRateLimiter<HttpContext> RateLimiter { get; }
}

}
69 changes: 69 additions & 0 deletions src/Playground/Middleware/RateLimitMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Threading.RateLimiting;
using System.Collections.Generic;
using Microsoft.Extensions.Options;

namespace Playground
{
public class RateLimitMiddleware
{
private readonly RequestDelegate _next;
private readonly PartitionedOptions _options;

public RateLimitMiddleware(RequestDelegate next, IOptions<PartitionedOptions> options)
{
_options = options.Value;
_next = next;
}

public async Task Invoke(HttpContext context)
{
var endpoint = context.GetEndpoint();
if (endpoint == null)
{
await _next(context);
return;
}

if (endpoint.Metadata.GetMetadata<IDisableRateLimitMetadata>() is not null)
{
await _next(context);
return;
}

var policies = endpoint.Metadata.GetOrderedMetadata<IRateLimitPolicy>();
var IsAcquired = true;
foreach (var policy in policies)
{
if (policy is IRateLimitPolicy rateLimitPolicy)
{
using var lease = await _options.Policies[rateLimitPolicy.PolicyName].WaitAsync(context);
if (!lease.IsAcquired)
{
IsAcquired = false;
break;
}
}
else if (policy is IRateLimitInline rateLimitInline)
{
using var lease = await rateLimitInline.RateLimiter.WaitAsync(context);
if (!lease.IsAcquired)
{
IsAcquired = false;
break;
}
}
}

if (IsAcquired)
{
await _next(context);
return;
}
else
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
//OnRejected(...)
}
}
}
}
13 changes: 13 additions & 0 deletions src/Playground/Middleware/RateLimitPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Playground
{
public class RateLimitPolicy : IRateLimitPolicy
{
public RateLimitPolicy(string policyName)
{
PolicyName = policyName;
}

public string PolicyName { get; }
}

}
34 changes: 34 additions & 0 deletions src/Playground/Middleware/TokenBucketRateLimitInline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Threading.RateLimiting;

namespace Playground
{
public class TokenBucketRateLimitInline : IRateLimitInline
{
public TokenBucketRateLimitInline(int tokenlimit, TimeSpan replenishmentPeriod, int tokensPerPeriod)
: this(_ => string.Empty, tokenlimit, replenishmentPeriod, tokensPerPeriod)
{

}

public TokenBucketRateLimitInline(Func<HttpContext, string> partitioner, int tokenlimit, TimeSpan replenishmentPeriod, int tokensPerPeriod)
{
RateLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var partition = partitioner(context);
return RateLimitPartition.CreateTokenBucketLimiter<string>(partition, key =>
{
return new TokenBucketRateLimiterOptions(
tokenLimit: tokenlimit,
queueProcessingOrder: QueueProcessingOrder.OldestFirst,
queueLimit: 0,
replenishmentPeriod: replenishmentPeriod,
tokensPerPeriod: tokensPerPeriod
);
});
});
}

public PartitionedRateLimiter<HttpContext> RateLimiter { get; }
}

}
13 changes: 13 additions & 0 deletions src/Playground/Playground.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.RateLimiting" Version="7.0.0-preview.6.22330.3" />
</ItemGroup>

</Project>
50 changes: 50 additions & 0 deletions src/Playground/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Playground;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);


var app = builder.Build();

var options = new PartitionedOptions();
options.AddPolicy<string>("Path", context =>
{
var endpointName = context.GetEndpoint()?.DisplayName ?? "";
return new RateLimitPartition<string>(endpointName, name =>
{
return new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(2, QueueProcessingOrder.OldestFirst, 0, TimeSpan.FromMinutes(1)));
});
});

options.AddPolicy<string>("User", context =>
{
var userId = context.Request.Query["userId"].ToString();
return new RateLimitPartition<string>(userId, user =>
{
return new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions(7, QueueProcessingOrder.OldestFirst, 0, TimeSpan.FromMinutes(1)));
});
});

app.UseRateLimiting(options);

app.MapGet("/A", async context =>
{
await context.Response.WriteAsync(context.Request.Path);
}).RequireRateLimit("Path", "User");

app.MapGet("/B", async context =>
{
await context.Response.WriteAsync(context.Request.Path);
}).RequireRateLimit("Path", "User");

app.MapGet("/C", async context =>
{
await context.Response.WriteAsync(context.Request.Path);
}).RequireRateLimit("User");

app.MapGet("/D", async context =>
{
await context.Response.WriteAsync(context.Request.Path);
}).RequireRateLimit("User").RequireTokenBucketRateLimit(2, TimeSpan.FromSeconds(10), 1);

app.Run();
37 changes: 37 additions & 0 deletions src/Playground/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:35279",
"sslPort": 44389
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5010",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7253;http://localhost:5010",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}