-
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
BRIMIT\sla
committed
Jun 8, 2024
1 parent
73f3a7c
commit f519828
Showing
10 changed files
with
380 additions
and
16 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
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,171 @@ | ||
using System; | ||
using Microsoft.AspNetCore.Mvc; | ||
using RateLimiter.Api; | ||
using Xunit; | ||
|
||
namespace RateLimiter.Tests; | ||
|
||
public class RateLimiterControllerTests | ||
{ | ||
private readonly RateLimiterApiController _controller; | ||
|
||
public RateLimiterControllerTests() | ||
{ | ||
_controller = new RateLimiterApiController(); | ||
} | ||
|
||
[Theory] | ||
[InlineData(1)] | ||
[InlineData(9)] | ||
public void GetFixedLimit_AllowsRequest_ReturnsOk(int rateLimit) | ||
{ | ||
// Arrange | ||
const string clientId = "testClient1"; | ||
|
||
// Act | ||
IActionResult? result = null; | ||
for (int i = 0; i < rateLimit; i++) // <=10 calls in a min | ||
{ | ||
result = _controller.GetFixedLimit(clientId); | ||
} | ||
|
||
// Assert | ||
Assert.IsType<OkResult>(result); | ||
Assert.Equal(200, ((OkResult)result).StatusCode); | ||
} | ||
|
||
[Fact] | ||
public void GetFixedLimit_BlocksRequest_ReturnsTooManyRequests() | ||
{ | ||
// Arrange | ||
const string clientId = "testClient1"; | ||
|
||
// Act | ||
for (int i = 0; i < 10; i++) | ||
{ | ||
_controller.GetFixedLimit(clientId); | ||
} | ||
var result = _controller.GetFixedLimit(clientId); // 11th call in a min | ||
|
||
// Assert | ||
Assert.IsType<ObjectResult>(result); | ||
Assert.Equal(429, ((ObjectResult)result).StatusCode); | ||
Assert.Equal(RateLimiterApiController.TooManyRequestsMessage, ((ObjectResult)result).Value); | ||
} | ||
|
||
[Fact] | ||
public void GetSlidingLimit_AllowsRequest_ReturnsOk() | ||
{ | ||
// Arrange | ||
const string clientId = "testClient2"; | ||
|
||
// Act | ||
_controller.GetSlidingLimit(clientId); | ||
|
||
var endTime = DateTime.UtcNow.AddSeconds(5); // delay 5 sec | ||
while (DateTime.UtcNow < endTime) { } | ||
|
||
var result = _controller.GetSlidingLimit(clientId); | ||
|
||
// Assert | ||
Assert.IsType<OkResult>(result); | ||
Assert.Equal(200, ((OkResult)result).StatusCode); | ||
} | ||
|
||
[Fact] | ||
public void GetSlidingLimit_BlocksRequest_ReturnsTooManyRequests() | ||
{ | ||
// Arrange | ||
const string clientId = "testClient2"; | ||
|
||
// Act | ||
IActionResult? result = null; | ||
for (int i = 0; i < 2; i++) // 2 calls in 10 sec | ||
{ | ||
result = _controller.GetSlidingLimit(clientId); | ||
} | ||
|
||
// Assert | ||
Assert.IsType<ObjectResult>(result); | ||
Assert.Equal(429, ((ObjectResult)result).StatusCode); | ||
Assert.Equal(RateLimiterApiController.TooManyRequestsMessage, ((ObjectResult)result).Value); | ||
} | ||
|
||
[Theory] | ||
[InlineData(1)] | ||
[InlineData(9)] | ||
public void GetRegionBasedLimit_US_AllowsRequest_ReturnsOk(int rateLimit) | ||
{ | ||
// Arrange | ||
const string clientId = "US_testClient3"; | ||
|
||
// Act | ||
IActionResult? result = null; | ||
for (int i = 0; i < rateLimit; i++) // <=10 calls in a min | ||
{ | ||
result = _controller.GetRegionBasedLimit(clientId); | ||
} | ||
|
||
// Assert | ||
Assert.IsType<OkResult>(result); | ||
Assert.Equal(200, ((OkResult)result).StatusCode); | ||
} | ||
|
||
[Fact] | ||
public void GetRegionBasedLimit_US_BlocksRequest_ReturnsTooManyRequests() | ||
{ | ||
// Arrange | ||
const string clientId = "US_testClient3"; | ||
|
||
// Act | ||
for (int i = 0; i < 20; i++) | ||
{ | ||
_controller.GetRegionBasedLimit(clientId); | ||
} | ||
|
||
var result = _controller.GetRegionBasedLimit(clientId); // 21th call in a min | ||
|
||
// Assert | ||
Assert.IsType<ObjectResult>(result); | ||
Assert.Equal(429, ((ObjectResult)result).StatusCode); | ||
Assert.Equal(RateLimiterApiController.TooManyRequestsMessage, ((ObjectResult)result).Value); | ||
} | ||
|
||
[Fact] | ||
public void GetRegionBasedLimit_EU_AllowsRequest_ReturnsOk() | ||
{ | ||
// Arrange | ||
const string clientId = "EU_testClient3"; | ||
|
||
// Act | ||
_controller.GetRegionBasedLimit(clientId); | ||
|
||
var endTime = DateTime.UtcNow.AddSeconds(5); // delay 5 sec | ||
while (DateTime.UtcNow < endTime) { } | ||
|
||
var result = _controller.GetRegionBasedLimit(clientId); | ||
|
||
// Assert | ||
Assert.IsType<OkResult>(result); | ||
Assert.Equal(200, ((OkResult)result).StatusCode); | ||
} | ||
|
||
[Fact] | ||
public void GetRegionBasedLimit_EU_BlocksRequest_ReturnsTooManyRequests() | ||
{ | ||
// Arrange | ||
const string clientId = "EU_testClient3"; | ||
|
||
// Act | ||
IActionResult? result = null; | ||
for (int i = 0; i < 2; i++) // 2 calls in 10 sec | ||
{ | ||
result = _controller.GetRegionBasedLimit(clientId); | ||
} | ||
|
||
// Assert | ||
Assert.IsType<ObjectResult>(result); | ||
Assert.Equal(429, ((ObjectResult)result).StatusCode); | ||
Assert.Equal(RateLimiterApiController.TooManyRequestsMessage, ((ObjectResult)result).Value); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
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,49 @@ | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace RateLimiter.Api | ||
{ | ||
public class RateLimiterApiController : ControllerBase | ||
{ | ||
private readonly RateLimiter _rateLimiter; | ||
|
||
public const string TooManyRequestsMessage = "Too many requests"; | ||
|
||
public RateLimiterApiController() | ||
{ | ||
_rateLimiter = RateLimiter.Instance; | ||
} | ||
|
||
[HttpGet("fixedLimit")] | ||
public IActionResult GetFixedLimit(string clientId) | ||
{ | ||
if (!_rateLimiter.IsRequestAllowed(clientId, "fixedLimit")) | ||
{ | ||
return StatusCode(429, TooManyRequestsMessage); | ||
} | ||
|
||
return Ok(); | ||
} | ||
|
||
[HttpGet("slidingLimit")] | ||
public IActionResult GetSlidingLimit(string clientId) | ||
{ | ||
if (!_rateLimiter.IsRequestAllowed(clientId, "slidingLimit")) | ||
{ | ||
return StatusCode(429, TooManyRequestsMessage); | ||
} | ||
|
||
return Ok(); | ||
} | ||
|
||
[HttpGet("regionBasedLimit")] | ||
public IActionResult GetRegionBasedLimit(string clientId) | ||
{ | ||
if (!_rateLimiter.IsRequestAllowed(clientId, "regionLimit")) | ||
{ | ||
return StatusCode(429, TooManyRequestsMessage); | ||
} | ||
|
||
return Ok(); | ||
} | ||
} | ||
} |
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,47 @@ | ||
using RateLimiter.Rules; | ||
using RateLimiter.Rules.Interfaces; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace RateLimiter | ||
{ | ||
public class RateLimiter : Dictionary<string, List<ILimitRule>> | ||
{ | ||
private static readonly Lazy<RateLimiter> RateLimiterInstance = new(() => new RateLimiter()); | ||
|
||
private RateLimiter() | ||
{ | ||
LoadRules(); | ||
} | ||
|
||
public static RateLimiter Instance => RateLimiterInstance.Value; | ||
|
||
public void Add(string ruleName, ILimitRule rule) | ||
{ | ||
if (!this.ContainsKey(ruleName)) | ||
{ | ||
this[ruleName] = new List<ILimitRule>(); | ||
} | ||
|
||
this[ruleName].Add(rule); | ||
} | ||
|
||
public bool IsRequestAllowed(string clientId, string ruleName) | ||
{ | ||
return !this.ContainsKey(ruleName) || | ||
this[ruleName].All(rule => rule.IsRequestAllowed(clientId, ruleName)); | ||
} | ||
|
||
private void LoadRules() | ||
{ | ||
this.Add("fixedLimit", new FixedLimitRule(10, TimeSpan.FromMinutes(1))); | ||
this.Add("slidingLimit", new SlidingLimitRule(TimeSpan.FromSeconds(5))); | ||
this.Add( | ||
"regionLimit", | ||
new RegionBasedRule( | ||
new FixedLimitRule(20, TimeSpan.FromMinutes(1)), | ||
new SlidingLimitRule(TimeSpan.FromSeconds(5)))); | ||
} | ||
} | ||
} |
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
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,39 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using RateLimiter.Rules.Interfaces; | ||
|
||
namespace RateLimiter.Rules | ||
{ | ||
public class FixedLimitRule : ILimitRule | ||
{ | ||
private readonly int _limit; | ||
private readonly TimeSpan _timeSpan; | ||
private readonly Dictionary<string, List<DateTime>> _clientRequests = new(); | ||
|
||
public FixedLimitRule(int limit, TimeSpan timeSpan) | ||
{ | ||
_limit = limit; | ||
_timeSpan = timeSpan; | ||
} | ||
|
||
public bool IsRequestAllowed(string clientId, string ruleName) | ||
{ | ||
if (!_clientRequests.ContainsKey(clientId)) | ||
{ | ||
_clientRequests[clientId] = new List<DateTime>(); | ||
} | ||
|
||
var now = DateTime.UtcNow; | ||
_clientRequests[clientId].RemoveAll(entry => now - entry > _timeSpan); | ||
|
||
if (_clientRequests[clientId].Count >= _limit) | ||
{ | ||
return false; | ||
} | ||
|
||
_clientRequests[clientId].Add(now); | ||
|
||
return true; | ||
} | ||
} | ||
} |
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,7 @@ | ||
namespace RateLimiter.Rules.Interfaces | ||
{ | ||
public interface ILimitRule | ||
{ | ||
bool IsRequestAllowed(string clientId, string ruleName); | ||
} | ||
} |
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 @@ | ||
using RateLimiter.Rules.Interfaces; | ||
|
||
namespace RateLimiter.Rules | ||
{ | ||
public class RegionBasedRule : ILimitRule | ||
{ | ||
private readonly ILimitRule _usRule; | ||
private readonly ILimitRule _euRule; | ||
|
||
public RegionBasedRule(ILimitRule usRule, ILimitRule euRule) | ||
{ | ||
_usRule = usRule; | ||
_euRule = euRule; | ||
} | ||
|
||
public bool IsRequestAllowed(string clientId, string ruleName) | ||
{ | ||
string region = GetRegionFromClientId(clientId); | ||
|
||
return region switch | ||
{ | ||
"US" => _usRule.IsRequestAllowed(clientId, ruleName), | ||
"EU" => _euRule.IsRequestAllowed(clientId, ruleName), | ||
_ => true | ||
}; | ||
} | ||
|
||
private static string GetRegionFromClientId(string clientId) => clientId.StartsWith("US") ? "US" : "EU"; | ||
} | ||
} |
Oops, something went wrong.