-
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.
- Loading branch information
Showing
10 changed files
with
419 additions
and
9 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 |
---|---|---|
@@ -1,4 +1,5 @@ | ||
.vs | ||
.DS_Store | ||
packages | ||
bin | ||
obj |
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 @@ | ||
{ | ||
"version": "0.2.0", | ||
"configurations": [ | ||
{ | ||
// Use IntelliSense to find out which attributes exist for C# debugging | ||
// Use hover for the description of the existing attributes | ||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md | ||
"name": ".NET Core Launch (console)", | ||
"type": "coreclr", | ||
"request": "launch", | ||
"preLaunchTask": "build", | ||
// If you have changed target frameworks, make sure to update the program path. | ||
"program": "${workspaceFolder}/RateLimiter.Tests/bin/Debug/net6.0/RateLimiter.Tests.dll", | ||
"args": [], | ||
"cwd": "${workspaceFolder}/RateLimiter.Tests", | ||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console | ||
"console": "internalConsole", | ||
"stopAtEntry": false | ||
}, | ||
{ | ||
"name": ".NET Core Attach", | ||
"type": "coreclr", | ||
"request": "attach" | ||
} | ||
] | ||
} |
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,41 @@ | ||
{ | ||
"version": "2.0.0", | ||
"tasks": [ | ||
{ | ||
"label": "build", | ||
"command": "dotnet", | ||
"type": "process", | ||
"args": [ | ||
"build", | ||
"${workspaceFolder}/RateLimiter.sln", | ||
"/property:GenerateFullPaths=true", | ||
"/consoleloggerparameters:NoSummary;ForceNoAlign" | ||
], | ||
"problemMatcher": "$msCompile" | ||
}, | ||
{ | ||
"label": "publish", | ||
"command": "dotnet", | ||
"type": "process", | ||
"args": [ | ||
"publish", | ||
"${workspaceFolder}/RateLimiter.sln", | ||
"/property:GenerateFullPaths=true", | ||
"/consoleloggerparameters:NoSummary;ForceNoAlign" | ||
], | ||
"problemMatcher": "$msCompile" | ||
}, | ||
{ | ||
"label": "watch", | ||
"command": "dotnet", | ||
"type": "process", | ||
"args": [ | ||
"watch", | ||
"run", | ||
"--project", | ||
"${workspaceFolder}/RateLimiter.sln" | ||
], | ||
"problemMatcher": "$msCompile" | ||
} | ||
] | ||
} |
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 |
---|---|---|
@@ -1,13 +1,138 @@ | ||
using NUnit.Framework; | ||
|
||
namespace RateLimiter.Tests; | ||
using System; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using NUnit.Framework; | ||
using RateLimiterNS.RateLimitRules; | ||
|
||
[TestFixture] | ||
public class RateLimiterTest | ||
{ | ||
[Test] | ||
public void Example() | ||
{ | ||
Assert.That(true, Is.True); | ||
} | ||
} | ||
private RateLimiterNS.RateLimiter.RateLimiter? _rateLimiter; | ||
|
||
[SetUp] | ||
public void SetUp() | ||
{ | ||
string xmlFilePath = Path.Combine(AppContext.BaseDirectory, "RateLimiter.xml"); | ||
_rateLimiter = RateLimiterNS.RateLimiter.RateLimiter.LoadFromConfiguration(xmlFilePath); | ||
} | ||
|
||
[Test] | ||
public async Task ValidTokenWithMultipleRules_ShouldAllowRequests_UnderLimit() | ||
{ | ||
//max 5 requests per 1 minute and min 2 seconds between requests allowed | ||
string token = "token1"; | ||
|
||
for (int i = 0; i < 5; i++) | ||
{ | ||
await Task.Delay(TimeSpan.FromSeconds(3)); | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
} | ||
} | ||
|
||
[Test] | ||
public async Task ValidTokenWithMultipleRules_ShouldDenyRequests_OverLimit() | ||
{ | ||
//max 5 requests per 1 minute and min 2 seconds between requests allowed | ||
string token = "token1"; | ||
|
||
for (int i = 0; i < 5; i++) | ||
{ | ||
if (i == 0) | ||
{ | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
} | ||
Assert.IsFalse(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
} | ||
// The sixth request should be denied | ||
Assert.IsFalse(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
} | ||
|
||
[Test] | ||
public async Task ConcurrentRequests_ShouldBeHandledCorrectly() | ||
{ | ||
// max 3 requests per 1 minute allowed | ||
string token = "token3"; | ||
int allowedRequests = 3; | ||
var tasks = new List<Task<bool>>(); | ||
|
||
// Fire multiple requests concurrently | ||
for (int i = 0; i < 6; i++) | ||
{ | ||
tasks.Add(Task.Run(() => _rateLimiter!.IsRequestAllowedAsync(token))); | ||
} | ||
|
||
Task.WhenAll(tasks).Wait(); | ||
|
||
int allowedCount = tasks.Count(task => task.Result); | ||
Assert.IsTrue(allowedCount <= allowedRequests, $"Allowed requests exceeded: {allowedCount} > {allowedRequests}"); | ||
} | ||
|
||
[Test] | ||
public async Task ValidTokenWithMultipleRules_ShouldAllowRequests_AfterWait() | ||
{ | ||
//max 5 requests per 1 minute and min 2 seconds between requests allowed | ||
string token = "token1"; | ||
|
||
// First request -> should be allowed | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
// Second request (immediately) -> should be denied | ||
Assert.IsFalse(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
// Sleep for 2 seconds | ||
await Task.Delay(TimeSpan.FromSeconds(2)); | ||
// Now it should be allowed | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
} | ||
[Test] | ||
public async Task ValidTokenWithTimeSpanSinceLastRequestRule_ShouldEnforceRule() | ||
{ | ||
// min 2 seconds between requests allowed | ||
string token = "token2"; | ||
// First request -> should be allowed | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
// Second request (immediately) -> should be denied | ||
Assert.IsFalse(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
// Sleep for 2 seconds | ||
await Task.Delay(TimeSpan.FromSeconds(2)); | ||
// Now next requests should be allowed and denied | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
Assert.IsFalse(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
|
||
} | ||
|
||
[Test] | ||
public async Task ValidTokenWithRequestsPerTimeSpanRule_ShouldEnforceRule() | ||
{ | ||
// max 3 requests per 1 minute allowed | ||
string token = "token3"; | ||
|
||
// First request -> should be allowed | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
|
||
// Second request -> should be allowed | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
|
||
// Third request -> should be allowed | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
|
||
// Fourth request -> should be denied (exceeding the limit of 3 requests within 1 minute) | ||
Assert.IsFalse(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
|
||
// Wait for the timespan of 1 minute | ||
await Task.Delay(TimeSpan.FromMinutes(1)); | ||
|
||
// restriction should be lifted and next 3 requests should be allowed now and 4th request should be denied | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
Assert.IsTrue(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
Assert.IsFalse(await _rateLimiter!.IsRequestAllowedAsync(token)); | ||
} | ||
|
||
[Test] | ||
public async Task InvalidToken_ShouldDenyRequests() | ||
{ | ||
string token = "invalid_token"; | ||
Assert.IsFalse(await _rateLimiter!.IsRequestAllowedAsync(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,10 @@ | ||
using System; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiterNS.RateLimitRules | ||
{ | ||
public interface IRateLimitRule | ||
{ | ||
Task<bool> IsRequestAllowedAsync(string token, DateTime requestTime); | ||
} | ||
} |
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,50 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiterNS.RateLimitRules | ||
{ | ||
public class RequestsPerTimeSpanRule : IRateLimitRule | ||
{ | ||
private readonly int _maxRequests; | ||
private readonly TimeSpan _timespan; | ||
private readonly ConcurrentDictionary<string, List<DateTime>> _requestTimes = new(); | ||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); | ||
|
||
public RequestsPerTimeSpanRule(int maxRequests, TimeSpan timespan) | ||
{ | ||
_maxRequests = maxRequests; | ||
_timespan = timespan; | ||
} | ||
|
||
public async Task<bool> IsRequestAllowedAsync(string token, DateTime requestTime) | ||
{ | ||
var times = _requestTimes.GetOrAdd(token, _ => new List<DateTime>()); | ||
|
||
await _semaphore.WaitAsync(); | ||
try | ||
{ | ||
|
||
times.RemoveAll(t => t < requestTime - _timespan); | ||
|
||
|
||
if (times.Count >= _maxRequests) | ||
{ | ||
return false; | ||
} | ||
|
||
|
||
times.Add(requestTime); | ||
return true; | ||
|
||
} | ||
finally | ||
{ | ||
_semaphore.Release(); | ||
} | ||
} | ||
} | ||
|
||
} |
42 changes: 42 additions & 0 deletions
42
RateLimiter/RateLimitRules/TimeSpanSinceLastRequestRule.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,42 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using System.Xml.Linq; | ||
|
||
namespace RateLimiterNS.RateLimitRules | ||
{ | ||
public class TimeSpanSinceLastRequestRule : IRateLimitRule | ||
{ | ||
private readonly TimeSpan _minTimeSpan; | ||
private readonly ConcurrentDictionary<string, DateTime> _lastRequestTimes = new(); | ||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); | ||
|
||
public TimeSpanSinceLastRequestRule(TimeSpan minTimeSpan) | ||
{ | ||
_minTimeSpan = minTimeSpan; | ||
} | ||
|
||
public async Task<bool> IsRequestAllowedAsync(string token, DateTime requestTime) | ||
{ | ||
await _semaphore.WaitAsync(); | ||
try | ||
{ | ||
var lastRequestTime = _lastRequestTimes.GetValueOrDefault(token); | ||
|
||
if (lastRequestTime == default || requestTime - lastRequestTime >= _minTimeSpan) | ||
{ | ||
_lastRequestTimes[token] = requestTime; | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
finally | ||
{ | ||
_semaphore.Release(); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.