-
Notifications
You must be signed in to change notification settings - Fork 232
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
David Aboh - implement rate limiter library
- Loading branch information
1 parent
d0a5741
commit d0594a3
Showing
34 changed files
with
1,183 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace RateLimiter.Common.Enum; | ||
|
||
[Flags] | ||
public enum RateLimitingMethod | ||
{ | ||
None = 0, | ||
RequestsPerTimespan = 1 << 0, | ||
TimeSpanSinceLastRequest = 1 << 1, | ||
} |
6 changes: 6 additions & 0 deletions
6
RateLimiter.Common/Exceptions/InvalidRequestContextException.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
namespace RateLimiter.Common.Exceptions; | ||
|
||
public class InvalidRequestContextException(string message) : Exception(message) | ||
{ | ||
public InvalidRequestContextException() : this("The request context is not properly configured. It is missing required information.") { } | ||
} |
27 changes: 27 additions & 0 deletions
27
RateLimiter.Common/Exceptions/RateLimitExceededException.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
namespace RateLimiter.Common.Exceptions; | ||
|
||
public class RateLimitExceededException(string message) : Exception(message) | ||
{ | ||
public RateLimitExceededException(string resource, int requestCount, int maxRequests, TimeSpan timeSpan) : | ||
this($"Rate limit exceeded for requested resource ({resource}) : Only {maxRequests} requests allowed per {timeSpan.TotalSeconds} seconds.") | ||
{ | ||
Resource = resource; | ||
RequestCount = requestCount; | ||
MaxRequests = maxRequests; | ||
TimeSpan = timeSpan; | ||
} | ||
|
||
public RateLimitExceededException(string resource, TimeSpan timeSinceLastRequest, int maxRequests, TimeSpan timeSpan) : | ||
this($"Rate limit exceeded for requested resource ({resource}) : Minimum {timeSinceLastRequest.TotalSeconds} seconds required between requests.") | ||
{ | ||
TimeSinceLastRequest = timeSinceLastRequest; | ||
MaxRequests = maxRequests; | ||
TimeSpan = timeSpan; | ||
} | ||
|
||
public string Resource { get; } = string.Empty; | ||
public int RequestCount { get; } | ||
public int MaxRequests { get; } | ||
public TimeSpan TimeSpan { get; } | ||
public TimeSpan TimeSinceLastRequest { get; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using RateLimiter.Common.Enum; | ||
|
||
namespace RateLimiter.Common.Model; | ||
|
||
public record RateLimitingOptions | ||
{ | ||
/// <summary> | ||
/// The configured method / rule. An enum flag that can contain multiple rules | ||
/// </summary> | ||
public RateLimitingMethod Method { get; set; } = RateLimitingMethod.RequestsPerTimespan; | ||
|
||
/// <summary> | ||
/// The maximum number of requests allowed with the specific method. | ||
/// </summary> | ||
public int MaxRequests { get; set; } = 1; | ||
|
||
/// <summary> | ||
/// Configured Timespan for this RateLimiting Method | ||
/// </summary> | ||
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromSeconds(1); | ||
|
||
/// <summary> | ||
/// The set of origin country codes that this rate limiting method is configured to apply to | ||
/// </summary> | ||
public HashSet<string>? ApplicableCountryCodes; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
namespace RateLimiter.Common.Model; | ||
|
||
/// <summary> | ||
/// Represents Context Info relevant to the current request scope. | ||
/// This contains all the configuration neccessary to perform rate limiting for the request. | ||
/// </summary> | ||
public record RequestContext() | ||
{ | ||
public RateLimitingOptions[] Options { get; set; } = []; | ||
|
||
public string Resource { get; set; } = string.Empty; | ||
|
||
/// <summary> | ||
/// Source Token of the Client making request | ||
/// </summary> | ||
public string Token { get; set; } = string.Empty; | ||
|
||
/// <summary> | ||
/// Two-Letter (Alpha-2) Iso Code that represents the country where the request originates | ||
/// </summary> | ||
public string? OriginIsoCountryCode { get; set; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
namespace RateLimiter.Common.Model; | ||
|
||
/// <summary> | ||
/// Represents request(s) from a source token at a given time. | ||
/// </summary> | ||
/// <param name="Token"></param> | ||
public record Traffic(string Resource, string Token) | ||
{ | ||
/// <summary> | ||
/// Identifier for the resource that is being requested. | ||
/// </summary> | ||
public string Resource { get; set; } = Resource; | ||
|
||
/// <summary> | ||
/// The token uniquely identifies the traffic source. Traffic entries with the same token came from the same source. | ||
/// </summary> | ||
public string Token { get; init; } = Token; | ||
|
||
/// <summary> | ||
/// The time the traffic was recorded | ||
/// </summary> | ||
public DateTime Time { get; init; } = DateTime.UtcNow; | ||
|
||
/// <summary> | ||
/// The number of requests associated with this traffic. Usually 1 but could be 2 if multiple requests happen at the same exact time. | ||
/// </summary> | ||
public int Requests { get; init; } = 1; | ||
} | ||
|
||
public record TrafficKey(string Resource, string Token); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
</Project> |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
|
||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Hosting; | ||
using RateLimiter.Repository.TrafficRepository; | ||
|
||
namespace RateLimiter.Repository; | ||
|
||
public static class Configure | ||
{ | ||
public static void ConfigureInMemoryRepository(this IHostApplicationBuilder builder) | ||
{ | ||
builder.Services.AddSingleton<ITrafficRepository, InMemoryTrafficRepository>(); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
global using RateLimiter.Common.Model; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\RateLimiter.Common\RateLimiter.Common.csproj" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" /> | ||
</ItemGroup> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
</Project> |
46 changes: 46 additions & 0 deletions
46
RateLimiter.Repository/TrafficRepository/ITrafficRepository.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
namespace RateLimiter.Repository.TrafficRepository; | ||
|
||
public interface ITrafficRepository | ||
{ | ||
/// <summary> | ||
/// Record new traffic. | ||
/// </summary> | ||
/// <param name="traffic">The traffic to record. </param> | ||
/// <returns></returns> | ||
public Task RecordTraffic(Traffic traffic); | ||
|
||
/// <summary> | ||
/// Get the last recorded traffic entry. | ||
/// </summary> | ||
/// <param name="for_token"> the source (token) of the traffic. </param> | ||
/// <param name="for_resource"> the identifier for the resource being requested </param> | ||
/// <returns></returns> | ||
public Task<Traffic?> GetLatestTraffic(string for_token, string for_resource); | ||
|
||
/// <summary> | ||
/// Get all the traffic recorded within the specific time span, optionally limited to a specific timespan. | ||
/// </summary> | ||
/// <param name="for_token"> the source (token) of the traffic </param> | ||
/// <param name="for_resource"> the identifier for the resource being requested </param> | ||
/// <param name="within_span"> optional filter operation to traffic within the specific time span. </param> | ||
/// <returns></returns> | ||
public Task<IEnumerable<Traffic>> GetTraffic(string for_token, string for_resource, TimeSpan? within_span = null); | ||
|
||
/// <summary> | ||
/// Get the Total number of Requests from the source (token). | ||
/// </summary> | ||
/// <param name="for_token"> the source (token) of the traffic, optionally limited to a specific timespan. </param> | ||
/// <param name="for_resource"> the identifier for the resource being requested </param> | ||
/// <param name="within_span"> optional filter operation to traffic within the specific time span. </param> | ||
/// <returns></returns> | ||
public Task<int> CountTraffic(string for_token, string for_resource, TimeSpan? within_span = default); | ||
|
||
/// <summary> | ||
/// Expire / Delete all traffic from the source (token), optionally limited to a specific timespan. | ||
/// </summary> | ||
/// <param name="for_token"> the source (token) of the traffic. </param> | ||
/// <param name="for_resource"> the identifier for the resource being requested </param> | ||
/// <param name="within_span"> optional filter operation to traffic within the specific time span. </param> | ||
/// <returns> number of traffic entries affected/expired </returns> | ||
public Task<int> ExpireTraffic(string for_token, string for_resource, TimeSpan? within_span = default); | ||
} |
92 changes: 92 additions & 0 deletions
92
RateLimiter.Repository/TrafficRepository/InMemoryTrafficRepository.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
using System.Collections.Concurrent; | ||
|
||
namespace RateLimiter.Repository.TrafficRepository; | ||
|
||
public class InMemoryTrafficRepository : ITrafficRepository | ||
{ | ||
private readonly ConcurrentDictionary<TrafficKey, ConcurrentQueue<Traffic>> _traffic = new(); | ||
private readonly object _lock = new(); | ||
#region ITrafficRepository | ||
|
||
public Task RecordTraffic(Traffic traffic) | ||
{ | ||
lock (_lock) | ||
{ | ||
TrafficKey key = new(traffic.Resource, traffic.Token); | ||
if (!_traffic.TryGetValue(key, out ConcurrentQueue<Traffic>? queue)) | ||
{ | ||
queue = new ConcurrentQueue<Traffic>(); | ||
_traffic[key] = queue; | ||
} | ||
|
||
queue.Enqueue(traffic); | ||
} | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
public Task<Traffic?> GetLatestTraffic(string token, string resource) | ||
{ | ||
if (_traffic.TryGetValue(new(resource, token), out ConcurrentQueue<Traffic>? queue) && queue.TryPeek(out Traffic? latestTraffic)) | ||
{ | ||
return Task.FromResult<Traffic?>(latestTraffic); | ||
} | ||
return Task.FromResult<Traffic?>(null); | ||
} | ||
|
||
public Task<IEnumerable<Traffic>> GetTraffic(string token, string resource, TimeSpan? within_span = null) | ||
{ | ||
if (!_traffic.TryGetValue(new(resource, token), out ConcurrentQueue<Traffic>? queue)) | ||
{ | ||
return Task.FromResult(Enumerable.Empty<Traffic>()); | ||
} | ||
|
||
IEnumerable<Traffic> result = queue | ||
.Where(t => TrafficIsWithinTimeSpan(t, within_span)) | ||
.AsEnumerable(); | ||
|
||
return Task.FromResult(result); | ||
} | ||
|
||
public Task<int> CountTraffic(string token, string resource, TimeSpan? within_span = null) | ||
{ | ||
if (!_traffic.TryGetValue(new(resource, token), out ConcurrentQueue<Traffic>? queue)) | ||
{ | ||
return Task.FromResult(0); | ||
} | ||
|
||
int count = queue | ||
.Where(t => TrafficIsWithinTimeSpan(t, within_span)) | ||
.Sum(t => t.Requests); | ||
|
||
return Task.FromResult(count); | ||
} | ||
|
||
public Task<int> ExpireTraffic(string token, string resource, TimeSpan? within_span = null) | ||
{ | ||
if (!_traffic.TryGetValue(new(resource, token), out ConcurrentQueue<Traffic>? queue)) | ||
{ | ||
return Task.FromResult(0); | ||
} | ||
|
||
int affected = 0; | ||
|
||
while (queue.TryPeek(out Traffic? t) && TrafficIsWithinTimeSpan(t, within_span)) | ||
{ | ||
if (queue.TryDequeue(out _)) | ||
{ | ||
affected++; | ||
} | ||
} | ||
|
||
return Task.FromResult(affected); | ||
} | ||
#endregion | ||
|
||
#region Utility | ||
private static bool TrafficIsWithinTimeSpan(Traffic traffic, TimeSpan? span) | ||
{ | ||
return !span.HasValue || (DateTime.UtcNow - traffic.Time) <= span.Value; | ||
} | ||
#endregion Utility | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
|
||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Hosting; | ||
using RateLimiter.Services.Limiters; | ||
using RateLimiter.Services.RateLimitingService; | ||
using RateLimiter.Services.RequestContextService; | ||
|
||
namespace RateLimiter.Services; | ||
|
||
public static class Configure | ||
{ | ||
public static void ConfigureRateLimiterServices(this IHostApplicationBuilder builder) | ||
{ | ||
Repository.Configure.ConfigureInMemoryRepository(builder); | ||
builder.Services.AddScoped<IRequestContextService, ScopedRequestContextService>(); | ||
builder.Services.AddScoped<IRateLimitingService, RateLimitingService.RateLimitingService>(); | ||
builder.Services.AddScoped<FixedRequestsPerTimeSpanLimiter>(); | ||
builder.Services.AddScoped<TimeSpanSinceLastRequestLimiter>(); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
global using RateLimiter.Common.Model; | ||
global using RateLimiter.Common.Enum; | ||
global using RateLimiter.Common.Exceptions; |
22 changes: 22 additions & 0 deletions
22
RateLimiter.Services/Limiters/FixedRequestsPerTimeSpanLimiter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using RateLimiter.Common.Exceptions; | ||
using RateLimiter.Repository.TrafficRepository; | ||
|
||
namespace RateLimiter.Services.Limiters; | ||
|
||
public class FixedRequestsPerTimeSpanLimiter(ITrafficRepository trafficRepository) : ILimiter | ||
{ | ||
public virtual async Task Limit(RateLimitingOptions options, RequestContext context) | ||
{ | ||
if (options.MaxRequests <= 0 || options.TimeSpan == TimeSpan.Zero) | ||
{ | ||
throw new ArgumentException("Invalid rate limiting configuration: MaxRequests and TimeSpan must be greater than zero."); | ||
} | ||
|
||
var requestCount = await trafficRepository.CountTraffic(context.Token, context.Resource, options.TimeSpan); | ||
|
||
if (requestCount >= options.MaxRequests) | ||
{ | ||
throw new RateLimitExceededException(context.Resource, requestCount, options.MaxRequests, options.TimeSpan); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
namespace RateLimiter.Services.Limiters; | ||
|
||
public interface ILimiter | ||
{ | ||
/// <summary> | ||
/// Peform a specific Rate Limiting Rule for the provided options | ||
/// </summary> | ||
/// <returns></returns> | ||
Task Limit(RateLimitingOptions options, RequestContext context); | ||
} |
Oops, something went wrong.