Skip to content
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,15 @@ If you have any questions or concerns, please submit them as a [GitHub issue](ht
You should [fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) the project and [create a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) named as `FirstName LastName` once you are finished.

Good luck!

___________________________

Here is a bried overview of the added classes:

RateLimitingManager helps to configure the set of rules for a given resource.

IRateLimitRule is an interface for all rules which allows flexible control over the set of rules.

CoolingPeriodRule can be used to control if the amount of time passed since the previous request (from the same resource and client) is too small and the request should be blocked.

FixedWindowRule helps to restrict the amount of allowed requests over the selected period of time.
87 changes: 81 additions & 6 deletions RateLimiter.Tests/RateLimiterTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,88 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using NUnit.Framework;
using RateLimiter.RateLimitRules;

namespace RateLimiter.Tests;

[TestFixture]
public class RateLimiterTest
{
[Test]
public void Example()
{
Assert.That(true, Is.True);
}
[TestFixture]
public class RateLimiterTests
{
private RateLimitingManager _rateLimitingManager;

[SetUp]
public void Setup()
{
_rateLimitingManager = new RateLimitingManager();
}

[Test]
public void Test_FixedWindowRule_AllowsRequestWithinLimit()
{
var rule = new FixedWindowRule(maxRequests: 3, windowSize: TimeSpan.FromSeconds(10));
_rateLimitingManager.ConfigureResourceRules("TestResource", new List<IRateLimitRule> { rule });

var context = new RequestContext("TestClient", "TestResource");

Assert.IsTrue(_rateLimitingManager.IsRequestAllowed(context));
Assert.IsTrue(_rateLimitingManager.IsRequestAllowed(context));
Assert.IsTrue(_rateLimitingManager.IsRequestAllowed(context));
}

[Test]
public void Test_FixedWindowRule_RejectsRequestExceedingLimit()
{
var rule = new FixedWindowRule(maxRequests: 3, windowSize: TimeSpan.FromSeconds(10));
_rateLimitingManager.ConfigureResourceRules("TestResource", new List<IRateLimitRule> { rule });

var context = new RequestContext("TestClient", "TestResource");

_rateLimitingManager.IsRequestAllowed(context);
_rateLimitingManager.IsRequestAllowed(context);
_rateLimitingManager.IsRequestAllowed(context);

Assert.IsFalse(_rateLimitingManager.IsRequestAllowed(context));
}

[Test]
public void Test_CoolingPeriodRule_AllowsRequestAfterCoolingPeriod()
{
var rule = new CoolingPeriodRule(TimeSpan.FromSeconds(5));
_rateLimitingManager.ConfigureResourceRules("TestResource", new List<IRateLimitRule> { rule });

var context = new RequestContext("TestClient", "TestResource");

Assert.IsTrue(_rateLimitingManager.IsRequestAllowed(context));
Assert.IsFalse(_rateLimitingManager.IsRequestAllowed(context));

System.Threading.Thread.Sleep(5000);

Assert.IsTrue(_rateLimitingManager.IsRequestAllowed(context));
}

[Test]
public void Test_CombinedRules_AllowsRequestWhenBothRulesPass()
{
var fixedWindowRule = new FixedWindowRule(maxRequests: 3, windowSize: TimeSpan.FromSeconds(10));
var coolingPeriodRule = new CoolingPeriodRule(TimeSpan.FromSeconds(5));
_rateLimitingManager.ConfigureResourceRules("TestResource", [fixedWindowRule, coolingPeriodRule]);

var context = new RequestContext("TestClient", "TestResource");

Assert.IsTrue(_rateLimitingManager.IsRequestAllowed(context));
Assert.IsFalse(_rateLimitingManager.IsRequestAllowed(context));

System.Threading.Thread.Sleep(5000);

Assert.IsTrue(_rateLimitingManager.IsRequestAllowed(context));

_rateLimitingManager.IsRequestAllowed(context);
_rateLimitingManager.IsRequestAllowed(context);

Assert.IsFalse(_rateLimitingManager.IsRequestAllowed(context));
}
}
}
8 changes: 8 additions & 0 deletions RateLimiter/IRateLimitRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace RateLimiter;

public interface IRateLimitRule
{
bool IsRequestAllowed(RequestContext context, DateTime requestTime);
}
31 changes: 31 additions & 0 deletions RateLimiter/RateLimitRules/CoolingPeriodRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;

namespace RateLimiter.RateLimitRules;

public class CoolingPeriodRule: IRateLimitRule
{
private readonly TimeSpan _minimumInterval;

private readonly Dictionary<(string clientId, string resource), DateTime> _lastRequest = new();

public CoolingPeriodRule(TimeSpan minimumInterval)
{
_minimumInterval = minimumInterval;
}

public bool IsRequestAllowed(RequestContext context, DateTime requestTime)
{
var key = (context.ClientId, context.Resource);
if (_lastRequest.TryGetValue(key, out var lastTime))
{
if (requestTime - lastTime < _minimumInterval)
{
return false;
}
}

_lastRequest[key] = requestTime;
return true;
}
}
39 changes: 39 additions & 0 deletions RateLimiter/RateLimitRules/FixedWindowRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;

namespace RateLimiter.RateLimitRules;

public class FixedWindowRule: IRateLimitRule
{
private readonly int _maxRequests;
private readonly TimeSpan _windowSize;

private readonly Dictionary<(string clientId, string resource), List<DateTime>> _requestsInfo = new();

public FixedWindowRule(int maxRequests, TimeSpan windowSize)
{
_maxRequests = maxRequests;
_windowSize = windowSize;
}

public bool IsRequestAllowed(RequestContext context, DateTime requestTime)
{
var key = (context.ClientId, context.Resource);

if (!_requestsInfo.ContainsKey(key))
{
_requestsInfo[key] = new List<DateTime>();
}

var earliestAllowedTime = requestTime - _windowSize;
_requestsInfo[key].RemoveAll(dt => dt < earliestAllowedTime);

if (_requestsInfo[key].Count >= _maxRequests)
{
return false;
}

_requestsInfo[key].Add(requestTime);
return true;
}
}
20 changes: 20 additions & 0 deletions RateLimiter/RateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace RateLimiter;

public class RateLimiter
{
private readonly List<IRateLimitRule> _rules;

public RateLimiter(List<IRateLimitRule> rules)
{
_rules = rules ?? [];
}
public bool IsRequestAllowed(RequestContext context)
{
var now = DateTime.UtcNow;
return _rules.All(rule => rule.IsRequestAllowed(context, now));
}
}
18 changes: 18 additions & 0 deletions RateLimiter/RateLimitingManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Collections.Generic;

namespace RateLimiter;

public class RateLimitingManager
{
private readonly Dictionary<string, RateLimiter> _resourceLimiters = new();

public void ConfigureResourceRules(string resource, List<IRateLimitRule> rules)
{
_resourceLimiters[resource] = new RateLimiter(rules);
}

public bool IsRequestAllowed(RequestContext context)
{
return !_resourceLimiters.TryGetValue(context.Resource, out var limiter) || limiter.IsRequestAllowed(context);
}
}
14 changes: 14 additions & 0 deletions RateLimiter/RequestContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace RateLimiter;

public class RequestContext
{
public string ClientId { get; }

public string Resource { get; }

public RequestContext(string clientId, string resource)
{
ClientId = clientId;
Resource = resource;
}
}