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

David Aboh #251

Closed
wants to merge 1 commit 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
Binary file added .DS_Store
Binary file not shown.
9 changes: 9 additions & 0 deletions RateLimiter.Common/Enum/RateLimitingMethod.cs
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,
}
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 RateLimiter.Common/Exceptions/RateLimitExceededException.cs
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; }
}
26 changes: 26 additions & 0 deletions RateLimiter.Common/Model/RateLimitingOptions.cs
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;
}
22 changes: 22 additions & 0 deletions RateLimiter.Common/Model/RequestContext.cs
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; }
}
30 changes: 30 additions & 0 deletions RateLimiter.Common/Model/Traffic.cs
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);
9 changes: 9 additions & 0 deletions RateLimiter.Common/RateLimiter.Common.csproj
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 added RateLimiter.Repository/.DS_Store
Binary file not shown.
15 changes: 15 additions & 0 deletions RateLimiter.Repository/Configure.cs
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>();
}
}

1 change: 1 addition & 0 deletions RateLimiter.Repository/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using RateLimiter.Common.Model;
17 changes: 17 additions & 0 deletions RateLimiter.Repository/RateLimiter.Repository.csproj
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 RateLimiter.Repository/TrafficRepository/ITrafficRepository.cs
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);
}
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
}
21 changes: 21 additions & 0 deletions RateLimiter.Services/Configure.cs
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>();
}
}

3 changes: 3 additions & 0 deletions RateLimiter.Services/GlobalUsings.cs
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 RateLimiter.Services/Limiters/FixedRequestsPerTimeSpanLimiter.cs
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);
}
}
}
10 changes: 10 additions & 0 deletions RateLimiter.Services/Limiters/ILimiter.cs
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);
}
Loading
Loading