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

Create rate limit rules and tests for them #182

Closed
wants to merge 1 commit into from
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
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
Rate limiting involves restricting the number of requests that can be made by a client.
A client is identified with an access token, which is used for every request to a resource.
To prevent abuse of the server, APIs enforce rate-limiting techniques.
Based on the client, the rate-limiting application can decide whether to allow the request to go through or not.
The client makes an API call to a particular resource; the server checks whether the request for this client is within the limit.
Based on the client, the rate-limiting application can decide whether to allow the request to go
through or not.
The client makes an API call to a particular resource; the server checks whether the request for
this client is within the limit.
If the request is within the limit, then the request goes through.
Otherwise, the API call is restricted.

Expand All @@ -13,15 +15,26 @@ Some examples of request-limiting rules (you could imagine any others)
* a certain timespan passed since the last call;
* for US-based tokens, we use X requests per timespan, for EU-based - certain timespan passed since the last call.

The goal is to design a class(-es) that manage rate limits for every provided API resource by a set of provided *configurable and extendable* rules. For example, for one resource you could configure the limiter to use Rule A, for another one - Rule B, for a third one - both A + B, etc. Any combinations of rules should be possible, keep this fact in mind when designing the classes.
The goal is to design a class(-es) that manage rate limits for every provided API resource by a set
of provided *configurable and extendable* rules. For example, for one resource you could configure
the limiter to use Rule A, for another one - Rule B, for a third one - both A + B, etc.
Any combinations of rules should be possible, keep this fact in mind when designing the classes.

We're more interested in the design itself than in some smart and tricky rate limiting algorithm. There is no need to use neither database (in-memory storage is fine) nor any web framework. Do not waste time on preparing complex environment, reusable class library covered by a set of tests is more than enough.
We're more interested in the design itself than in some smart and tricky rate limiting algorithm.
There is no need to use neither database (in-memory storage is fine) nor any web framework.
Do not waste time on preparing complex environment, reusable class library covered by a set of tests
is more than enough.

There is a Test Project set up for you to use. You are welcome to create your own test project and use whatever test runner you would like.
There is a Test Project set up for you to use. You are welcome to create your own test project and
use whatever test runner you would like.

You are welcome to ask any questions regarding the requirements - treat us as product owners/analysts/whoever who knows the business.
Should you have any questions or concerns, submit them as a [GitHub issue](https://github.com/crexi-dev/rate-limiter/issues).
You are welcome to ask any questions regarding the requirements - treat us as product
owners/analysts/whoever who knows the business.
Should you have any questions or concerns, submit them as a
[GitHub issue](https://github.com/crexi-dev/rate-limiter/issues).

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) once you are finished.
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)
once you are finished.

Good luck!
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>diasble</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
Expand Down
114 changes: 106 additions & 8 deletions RateLimiter.Tests/RateLimiterTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,111 @@
using NUnit.Framework;

namespace RateLimiter.Tests;
using System;
using System.Collections.Generic;

[TestFixture]
public class RateLimiterTest
{
[Test]
public void Example()
{
Assert.That(true, Is.True);
}
}
private RateLimiter _rateLimiter;
private const string Resource = "resource1";

[SetUp]
public void SetUp()
{
_rateLimiter = new RateLimiter();
}

[Test]
public void FixedWindowRateLimitRule_AllowsRequestsWithinLimit()
{
var fixedRule = new FixedWindowRateLimitRule(5, TimeSpan.FromMinutes(1));
_rateLimiter.AddRule(Resource, fixedRule);

string client = "Client1";

for (int i = 0; i < 5; i++)
{
Assert.IsTrue(_rateLimiter.HandleRequest(client, Resource));
}
}

[Test]
public void FixedWindowRateLimitRule_DeniesRequestsExceedingLimit()
{
var fixedRule = new FixedWindowRateLimitRule(5, TimeSpan.FromMinutes(1));
_rateLimiter.AddRule(Resource, fixedRule);

string client = "Client1";

for (int i = 0; i < 5; i++)
{
Assert.IsTrue(_rateLimiter.HandleRequest(client, Resource));
}

// The 6th request should be denied
Assert.IsFalse(_rateLimiter.HandleRequest(client, Resource));
}

[Test]
public void SlidingWindowRateLimitRule_AllowsRequestAfterInterval()
{
var slidingRule = new SlidingWindowRateLimitRule(TimeSpan.FromSeconds(1));
_rateLimiter.AddRule(Resource, slidingRule);

string client = "Client1";

Assert.IsTrue(_rateLimiter.HandleRequest(client, Resource));

// Wait for the interval to pass
System.Threading.Thread.Sleep(1100);

Assert.IsTrue(_rateLimiter.HandleRequest(client, Resource));
}

[Test]
public void SlidingWindowRateLimitRule_DeniesRequestWithinInterval()
{
var slidingRule = new SlidingWindowRateLimitRule(TimeSpan.FromSeconds(5));
_rateLimiter.AddRule(Resource, slidingRule);

string client = "Client1";

Assert.IsTrue(_rateLimiter.HandleRequest(client, Resource));
Assert.IsFalse(_rateLimiter.HandleRequest(client, Resource));
}

[Test]
public void RegionBasedRateLimitRule_AppliesUSRule()
{
var regionRules = new Dictionary<string, IRateLimitRule>
{
{ "US", new FixedWindowRateLimitRule(2, TimeSpan.FromMinutes(1)) },
{ "EU", new SlidingWindowRateLimitRule(TimeSpan.FromSeconds(10)) }
};
var regionRule = new RegionBasedRateLimitRule(regionRules);
_rateLimiter.AddRule(Resource, regionRule);

string client = "USClient123";

Assert.IsTrue(_rateLimiter.HandleRequest(client, Resource));
Assert.IsTrue(_rateLimiter.HandleRequest(client, Resource));
Assert.IsFalse(_rateLimiter.HandleRequest(client, Resource));
}

[Test]
public void RegionBasedRateLimitRule_AppliesEURule()
{
var regionRules = new Dictionary<string, IRateLimitRule>
{
{ "US", new FixedWindowRateLimitRule(2, TimeSpan.FromMinutes(1)) },
{ "EU", new SlidingWindowRateLimitRule(TimeSpan.FromSeconds(2)) }
};
var regionRule = new RegionBasedRateLimitRule(regionRules);
_rateLimiter.AddRule(Resource, regionRule);

string client = "EUClient123";

Assert.IsTrue(_rateLimiter.HandleRequest(client, Resource));
System.Threading.Thread.Sleep(2100); // Wait for the interval to pass
Assert.IsTrue(_rateLimiter.HandleRequest(client, Resource));
}
}
8 changes: 4 additions & 4 deletions RateLimiter.sln
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.15
# Visual Studio Version 17
VisualStudioVersion = 17.10.34916.146
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.Tests", "RateLimiter.Tests\RateLimiter.Tests.csproj", "{C4F9249B-010E-46BE-94B8-DD20D82F1E60}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RateLimiter.Tests", "RateLimiter.Tests\RateLimiter.Tests.csproj", "{C4F9249B-010E-46BE-94B8-DD20D82F1E60}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B206889-9841-4B5E-B79B-D5B2610CCCFF}"
ProjectSection(SolutionItems) = preProject
Expand Down
25 changes: 25 additions & 0 deletions RateLimiter/FixedWindowRateLimitRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;

public class FixedWindowRateLimitRule(int maxRequests, TimeSpan timespan) : IRateLimitRule
{
private readonly int _maxRequests = maxRequests;
private readonly TimeSpan _timespan = timespan;

public bool IsRequestAllowed(string client,
string resource,
DateTime currentTime,
Dictionary<(string client, string resource), List<DateTime>> clientRequests)
{
var key = (client, resource);
if (!clientRequests.ContainsKey(key))
{
return true;
}

var windowStartTime = currentTime - _timespan;
var requestsInWindow = clientRequests[key].Where(req => req > windowStartTime).ToList();
return requestsInWindow.Count < _maxRequests;
}
}
11 changes: 11 additions & 0 deletions RateLimiter/IRateLimitRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;

public interface IRateLimitRule
{
bool IsRequestAllowed(
string client,
string resource,
DateTime currentTime,
Dictionary<(string client, string resource), List<DateTime>> clientRequests);
}
51 changes: 51 additions & 0 deletions RateLimiter/RateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;

public class RateLimiter
{
private readonly Dictionary<string, List<IRateLimitRule>> _resourceRules = [];
private readonly Dictionary<(string client, string resource), List<DateTime>> _clientRequests = [];

public void AddRule(string resource, IRateLimitRule rule)
{
if (!_resourceRules.ContainsKey(resource))
{
_resourceRules[resource] = new List<IRateLimitRule>();
}
_resourceRules[resource].Add(rule);
}

public bool IsRequestAllowed(string client, string resource)
{
var currentTime = DateTime.UtcNow;
if (!_resourceRules.ContainsKey(resource))
{
return true;
}

var rules = _resourceRules[resource];
return rules.All(rule => rule.IsRequestAllowed(client, resource, currentTime, _clientRequests));
}

public void RecordRequest(string client, string resource)
{
var currentTime = DateTime.UtcNow;
var key = (client, resource);
if (!_clientRequests.ContainsKey(key))
{
_clientRequests[key] = new List<DateTime>();
}
_clientRequests[key].Add(currentTime);
}

public bool HandleRequest(string client, string resource)
{
if (IsRequestAllowed(client, resource))
{
RecordRequest(client, resource);
return true;
}
return false;
}
}
29 changes: 29 additions & 0 deletions RateLimiter/RegionBasedRateLimitRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;

public class RegionBasedRateLimitRule(Dictionary<string, IRateLimitRule> regionRules) : IRateLimitRule
{
private readonly Dictionary<string, IRateLimitRule> _regionRules = regionRules;

public bool IsRequestAllowed(string client,
string resource,
DateTime currentTime,
Dictionary<(string client, string resource), List<DateTime>> clientRequests)
{
var region = GetRegionFromClient(client);
if (_regionRules.ContainsKey(region))
{
return _regionRules[region]
.IsRequestAllowed(client, resource, currentTime, clientRequests);
}

// Default to allowing the request if no specific rule for the region
return true;
}

private string GetRegionFromClient(string client)
{
// This is just a placeholder implementation.
return client.StartsWith("US") ? "US" : "EU";
}
}
23 changes: 23 additions & 0 deletions RateLimiter/SlidingWindowRateLimitRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;

public class SlidingWindowRateLimitRule(TimeSpan minInterval) : IRateLimitRule
{
private readonly TimeSpan _minInterval = minInterval;

public bool IsRequestAllowed(string client,
string resource,
DateTime currentTime,
Dictionary<(string client, string resource), List<DateTime>> clientRequests)
{
var key = (client, resource);
if (!clientRequests.ContainsKey(key))
{
return true;
}

var lastRequestTime = clientRequests[key].LastOrDefault();
return lastRequestTime == default || currentTime - lastRequestTime >= _minInterval;
}
}
32 changes: 32 additions & 0 deletions TestConsole/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
var rateLimiter = new RateLimiter();

var fixedRule = new FixedWindowRateLimitRule(4, TimeSpan.FromMinutes(1));
var slidingRule = new SlidingWindowRateLimitRule(TimeSpan.FromSeconds(1));
var regionRules = new Dictionary<string, IRateLimitRule>
{
{ "US", new FixedWindowRateLimitRule(5, TimeSpan.FromSeconds(1)) },
{ "EU", new SlidingWindowRateLimitRule(TimeSpan.FromSeconds(1)) }
};
var regionRule = new RegionBasedRateLimitRule(regionRules);

rateLimiter.AddRule("resource1", fixedRule);
rateLimiter.AddRule("resource2", slidingRule);
rateLimiter.AddRule("resource3", fixedRule);
rateLimiter.AddRule("resource3", slidingRule);
rateLimiter.AddRule("resource4", regionRule);

var client = "USClient123";
var resource = "resource4";

for (int i = 0; i < 12; i++)
{
Thread.Sleep(100);
if (rateLimiter.HandleRequest(client, resource))
{
Console.WriteLine("Request allowed.");
}
else
{
Console.WriteLine("Request denied due to rate limit.");
}
}
14 changes: 14 additions & 0 deletions TestConsole/TestConsole.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
</ItemGroup>

</Project>
Loading