-
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
1 parent
d0a5741
commit 38bf8c1
Showing
15 changed files
with
372 additions
and
8 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 |
---|---|---|
@@ -1,13 +1,105 @@ | ||
using NUnit.Framework; | ||
using System; | ||
using System.Linq; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using NUnit.Framework; | ||
using RateLimiter.Limiter; | ||
using RateLimiter.Storage; | ||
using RateLimiter.Models; | ||
using RateLimiter.Rules; | ||
|
||
namespace RateLimiter.Tests; | ||
|
||
[TestFixture] | ||
[Parallelizable(ParallelScope.All)] | ||
public class RateLimiterTest | ||
{ | ||
[Test] | ||
public void Example() | ||
{ | ||
Assert.That(true, Is.True); | ||
} | ||
[Test] | ||
public async Task ApplyRateLimitRules_ReturnsIsAllowedFalse_WhenLimitsExceeded() | ||
{ | ||
//Arrange | ||
var anyResource = "testResource"; | ||
|
||
var storage = new DefaultRateLimiterStorage<string>(); | ||
|
||
var slidingRuleRateLimiter = new RuleRateLimiter<string, string>( | ||
resource => | ||
{ | ||
var key = resource; | ||
|
||
var factory = new RateLimitRuleByKeyFactory<string> | ||
{ | ||
Key = key, | ||
LimitRule = _ => new SlidingWindowRule(2, TimeSpan.FromSeconds(3)) | ||
}; | ||
|
||
return factory; | ||
}, storage | ||
); | ||
|
||
var windowRuleRateLimiter = new RuleRateLimiter<string, string>( | ||
resource => | ||
{ | ||
var key = resource; | ||
|
||
var factory = new RateLimitRuleByKeyFactory<string> | ||
{ | ||
Key = key, | ||
LimitRule = _ => new FixedWindowRule(3, TimeSpan.FromSeconds(5)) | ||
}; | ||
|
||
return factory; | ||
}, storage | ||
); | ||
|
||
var list = new List<RuleRateLimiter<string, string>> | ||
{ | ||
slidingRuleRateLimiter, | ||
windowRuleRateLimiter | ||
}; | ||
|
||
var sut = new RateLimiter<string, string>(list); | ||
|
||
//Act | ||
var rateLimitResult1 = await sut.ApplyRateLimitRulesAsync(anyResource); | ||
var rateLimitResult2 = await sut.ApplyRateLimitRulesAsync(anyResource); | ||
var rateLimitResult3 = await sut.ApplyRateLimitRulesAsync(anyResource); | ||
var rateLimitResult4 = await sut.ApplyRateLimitRulesAsync(anyResource); | ||
|
||
await Task.Delay(TimeSpan.FromSeconds(7)); | ||
|
||
var rateLimitResult5 = await sut.ApplyRateLimitRulesAsync(anyResource); | ||
|
||
//Assert | ||
Assert.That(rateLimitResult1.IsAllowed); | ||
Assert.That(rateLimitResult1.RulesMessages, Is.All.Null); | ||
|
||
Assert.That(rateLimitResult2.IsAllowed); | ||
Assert.That(rateLimitResult2.RulesMessages, Is.All.Null); | ||
|
||
Assert.That(!rateLimitResult3.IsAllowed); | ||
Assert.That(rateLimitResult3.RulesMessages.Count(x => x != null) == 1); | ||
|
||
Assert.That(!rateLimitResult4.IsAllowed); | ||
Assert.That(rateLimitResult4.RulesMessages.Count(x => x != null) == 2); | ||
|
||
Assert.That(rateLimitResult5.IsAllowed); | ||
Assert.That(rateLimitResult5.RulesMessages, Is.All.Null); | ||
} | ||
|
||
[Test] | ||
public async Task ApplyRateLimitRules_ReturnsIsAllowedTrue_WhenTheRulesWereNotConfigured() | ||
{ | ||
//Arrange | ||
var anyResource = "testResource"; | ||
|
||
var sut = new RateLimiter<string, string>(new List<RuleRateLimiter<string, string>>()); | ||
|
||
//Act | ||
var rateLimitResult1 = await sut.ApplyRateLimitRulesAsync(anyResource); | ||
|
||
//Assert | ||
Assert.That(rateLimitResult1.IsAllowed); | ||
CollectionAssert.IsEmpty(rateLimitResult1.RulesMessages); | ||
} | ||
} |
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.Threading; | ||
using RateLimiter.Models; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiter.Limiter; | ||
|
||
public interface IRateLimiter<TResource> | ||
{ | ||
ValueTask<RateLimitResult> ApplyRateLimitRulesAsync(TResource resource, CancellationToken ct = default); | ||
} |
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.Threading; | ||
using RateLimiter.Models; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiter.Limiter; | ||
|
||
public interface IRuleRateLimiter<TResource, TKey> | ||
{ | ||
ValueTask<RuleResult> ApplyRateLimitRulesAsync(TResource resource, CancellationToken ct = default); | ||
} |
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,33 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using RateLimiter.Models; | ||
|
||
namespace RateLimiter.Limiter; | ||
|
||
public class RateLimiter<TResource, TKey> : IRateLimiter<TResource> | ||
{ | ||
private readonly IList<RuleRateLimiter<TResource, TKey>> _ruleRateLimiters; | ||
|
||
public RateLimiter(IList<RuleRateLimiter<TResource, TKey>> ruleRateLimiters) | ||
{ | ||
_ruleRateLimiters = ruleRateLimiters; | ||
} | ||
|
||
public async ValueTask<RateLimitResult> ApplyRateLimitRulesAsync(TResource resource, CancellationToken ct = default) | ||
{ | ||
var ruleRateLimiterTasks = _ruleRateLimiters | ||
.Select(ruleLimiter => ruleLimiter.ApplyRateLimitRulesAsync(resource, ct).AsTask()) | ||
.ToArray(); | ||
|
||
var rulesResults = await Task.WhenAll(ruleRateLimiterTasks); | ||
var result = new RateLimitResult | ||
{ | ||
IsAllowed = rulesResults.All(x => x.IsAllowed), | ||
RulesMessages = rulesResults.Select(x => x.RuleMessage).ToArray() | ||
}; | ||
|
||
return result; | ||
} | ||
} |
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 @@ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using RateLimiter.Models; | ||
using RateLimiter.Storage; | ||
|
||
namespace RateLimiter.Limiter; | ||
|
||
public class RuleRateLimiter<TResource, TKey> : IRuleRateLimiter<TResource, TKey> | ||
{ | ||
private readonly string _ruleRateLimiterId = Guid.NewGuid().ToString(); | ||
|
||
private readonly Func<TResource, RateLimitRuleByKeyFactory<TKey>> _ruleRateLimiter; | ||
private readonly IRateLimiterStorage<string> _rateLimiterStorage; | ||
|
||
public RuleRateLimiter( | ||
Func<TResource, RateLimitRuleByKeyFactory<TKey>> ruleRateLimiter, | ||
IRateLimiterStorage<string> rateLimiterStorage) | ||
{ | ||
_ruleRateLimiter = ruleRateLimiter; | ||
_rateLimiterStorage = rateLimiterStorage; | ||
} | ||
|
||
public async ValueTask<RuleResult> ApplyRateLimitRulesAsync(TResource resource, CancellationToken ct = default) | ||
{ | ||
var ruleRateLimiter = _ruleRateLimiter(resource); | ||
var key = $"{_ruleRateLimiterId}_{ruleRateLimiter.Key.GetHashCode()}"; | ||
|
||
var rule = await _rateLimiterStorage.GetRuleAsync(key, ct); | ||
if (rule == null) | ||
{ | ||
rule = ruleRateLimiter.LimitRule(ruleRateLimiter.Key); | ||
|
||
await _rateLimiterStorage.AddRateLimitRuleAsync(key, rule, ct); | ||
} | ||
|
||
var ruleResult = await rule.ApplyAsync(ct); | ||
|
||
return ruleResult; | ||
} | ||
} |
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,9 @@ | ||
using System.Collections.Generic; | ||
|
||
namespace RateLimiter.Models; | ||
|
||
public class RateLimitResult | ||
{ | ||
public bool IsAllowed { get; set; } | ||
public IList<string> RulesMessages { get; set; } | ||
} |
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,9 @@ | ||
using System; | ||
using RateLimiter.Rules; | ||
|
||
namespace RateLimiter.Models; | ||
public class RateLimitRuleByKeyFactory<TKey> | ||
{ | ||
public TKey Key { get; set; } | ||
public Func<TKey, IRateLimitRule> LimitRule { get; set; } | ||
} |
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.Models; | ||
|
||
public class RuleResult | ||
{ | ||
public bool IsAllowed { get; set; } | ||
public string RuleMessage { get; set; } | ||
} |
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,56 @@ | ||
using System; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using RateLimiter.Models; | ||
|
||
namespace RateLimiter.Rules; | ||
|
||
public class FixedWindowRule : IRateLimitRule | ||
{ | ||
private const string AccessForbiddenMessage = "Access forbidden for {0} seconds"; | ||
|
||
private readonly int _limit; | ||
private readonly TimeSpan _window; | ||
private DateTime _startTime; | ||
private int _count; | ||
private readonly object _lock = new(); | ||
|
||
public FixedWindowRule(int limit, TimeSpan window) | ||
{ | ||
_limit = limit; | ||
_window = window; | ||
_startTime = DateTime.UtcNow; | ||
_count = 0; | ||
} | ||
|
||
public ValueTask<RuleResult> ApplyAsync(CancellationToken ct = default) | ||
{ | ||
var now = DateTime.UtcNow; | ||
|
||
lock (_lock) | ||
{ | ||
if (now - _startTime >= _window) | ||
{ | ||
_startTime = now; | ||
_count = 1; | ||
return ValueTask.FromResult(new RuleResult { IsAllowed = true }); | ||
} | ||
|
||
if (_count >= _limit) | ||
{ | ||
var windowExpirationTime = _startTime + _window; | ||
var remainingTime = (windowExpirationTime - now).TotalSeconds; | ||
|
||
return ValueTask.FromResult(new RuleResult | ||
{ | ||
IsAllowed = false, | ||
RuleMessage = string.Format(AccessForbiddenMessage, $"{remainingTime:F2} ") | ||
}); | ||
} | ||
|
||
_count++; | ||
} | ||
|
||
return ValueTask.FromResult(new RuleResult { IsAllowed = 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,10 @@ | ||
using System.Threading; | ||
using RateLimiter.Models; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiter.Rules; | ||
|
||
public interface IRateLimitRule | ||
{ | ||
ValueTask<RuleResult> ApplyAsync(CancellationToken ct = default); | ||
} |
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,51 @@ | ||
using RateLimiter.Models; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace RateLimiter.Rules; | ||
|
||
public class SlidingWindowRule : IRateLimitRule | ||
{ | ||
private const string AccessForbiddenMessage = "Access forbidden for {0} seconds"; | ||
private readonly int _limit; | ||
private readonly TimeSpan _window; | ||
private readonly Queue<DateTime> _timestamps = new(); | ||
private readonly object _lock = new(); | ||
|
||
public SlidingWindowRule(int limit, TimeSpan window) | ||
{ | ||
_limit = limit; | ||
_window = window; | ||
} | ||
|
||
public ValueTask<RuleResult> ApplyAsync(CancellationToken ct = default) | ||
{ | ||
var now = DateTime.UtcNow; | ||
|
||
lock (_lock) | ||
{ | ||
while (_timestamps.Count > 0 && now - _timestamps.Peek() >= _window) | ||
{ | ||
_timestamps.Dequeue(); | ||
} | ||
|
||
if (_timestamps.Count >= _limit) | ||
{ | ||
var oldestRequestTime = _timestamps.Peek(); | ||
var remainingTime = (_window - (now - oldestRequestTime)).TotalSeconds; | ||
|
||
return ValueTask.FromResult(new RuleResult | ||
{ | ||
IsAllowed = false, | ||
RuleMessage = string.Format(AccessForbiddenMessage, $"{remainingTime:F2} ") | ||
}); | ||
} | ||
|
||
_timestamps.Enqueue(now); | ||
} | ||
|
||
return ValueTask.FromResult(new RuleResult { IsAllowed = true }); | ||
} | ||
} |
Oops, something went wrong.