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

Mikhail Batsian #278

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
2 changes: 1 addition & 1 deletion RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
Expand Down
104 changes: 98 additions & 6 deletions RateLimiter.Tests/RateLimiterTest.cs
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);
}
}
10 changes: 10 additions & 0 deletions RateLimiter/Limiter/IRateLimiter.cs
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);
}
10 changes: 10 additions & 0 deletions RateLimiter/Limiter/IRuleRateLimiter.cs
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);
}
33 changes: 33 additions & 0 deletions RateLimiter/Limiter/RateLimiter.cs
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;
}
}
41 changes: 41 additions & 0 deletions RateLimiter/Limiter/RuleRateLimiter.cs
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;
}
}
9 changes: 9 additions & 0 deletions RateLimiter/Models/RateLimitResult.cs
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; }
}
9 changes: 9 additions & 0 deletions RateLimiter/Models/RateLimitRuleByKeyFactory.cs
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; }
}
7 changes: 7 additions & 0 deletions RateLimiter/Models/RuleResult.cs
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; }
}
2 changes: 1 addition & 1 deletion RateLimiter/RateLimiter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Nullable>disable</Nullable>
</PropertyGroup>
</Project>
56 changes: 56 additions & 0 deletions RateLimiter/Rules/FixedWindowRule.cs
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 });
}
}
10 changes: 10 additions & 0 deletions RateLimiter/Rules/IRateLimitRule.cs
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);
}
51 changes: 51 additions & 0 deletions RateLimiter/Rules/SlidingWindowRule.cs
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 });
}
}
Loading