Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
d-j-kwak committed Oct 14, 2024
1 parent d0a5741 commit 319b57a
Show file tree
Hide file tree
Showing 10 changed files with 419 additions and 9 deletions.
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

0 comments on commit 319b57a

Please sign in to comment.