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 Kwak #244

Closed
wants to merge 2 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.vs
.DS_Store
packages
bin
obj
26 changes: 26 additions & 0 deletions .vscode/launch.json
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"
}
]
}
41 changes: 41 additions & 0 deletions .vscode/tasks.json
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"
}
]
}
143 changes: 134 additions & 9 deletions RateLimiter.Tests/RateLimiterTest.cs
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));
}
}
10 changes: 10 additions & 0 deletions RateLimiter/RateLimitRules/IRateLimitRule.cs
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);
}
}
50 changes: 50 additions & 0 deletions RateLimiter/RateLimitRules/RequestsPerTimeSpanRule.cs
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 RateLimiter/RateLimitRules/TimeSpanSinceLastRequestRule.cs
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();
}
}
}
}
Loading
Loading