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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ Some examples of request-limiting rules (you could imagine any others)
* a certain timespan has passed since the last call;
* For US-based tokens, we use X requests per timespan; for EU-based tokens, a certain timespan has passed since the last call.

The goal is to design a class(-es) that manages each API resource's rate limits 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 combination of rules should be possible; keep this fact in mind when designing the classes.
The goal is to design a class(-es) that manages each API resource's rate limits 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 combination 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 intelligent and tricky rate-limiting algorithm. There is no need to use a database (in-memory storage is fine) or 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 intelligent and tricky rate-limiting algorithm.
There is no need to use a database (in-memory storage is fine) or 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. However, you are welcome to create your own test project and use whatever test runner you like.

Expand Down
101 changes: 101 additions & 0 deletions RateLimiter.Tests/HelperRateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using RateLimiter.Data;
using RateLimiter.Log;
using RateLimiter.Model.Resource.Config;
using RateLimiter.Model.Rule.Config;
using RateLimiter.Resource;
using RateLimiter.Tracking;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;

namespace RateLimiter.Tests
{
internal class HelperRateLimiter
{
public const string EU_TOKEN = "eu-asdf-123-asdf";
public const string TOKEN = "xxvb-66-yui";

public const string IP_WHITE_LIST = "68.7.53.27";
public const string IP_BLACK_LIST = "68.7.53.99";

public const int DEFAULT_TIMESPAN_SECONDS = 30;
public const int DEFAULT_REQUEST_THRESHOLD = 5;

//Assuming you wire this up in a startup class you would hook the DI but just hand rolling instances for the sake of demonstration
public static ServiceRateLimiter GetServiceRateLimiter()
{
var repo = new RepositoryRateLimiter();
PopulateResourceConfigurations(repo);

return new ServiceRateLimiter(
new ServiceResourceConfigLoader(repo),
repo,
new ServiceLogger(),
new ServiceRequestTracker(repo));
}

private static void PopulateResourceConfigurations(RepositoryRateLimiter repo)
{
var configDefault = new ResourceConfig
{
ConfigId = 1,
Resource = Model.Resource.EnumResource.GetListings,
Rules = new List<RuleConfigBase>
{
new RuleConfigIpBlackList
{
IpAddresses = new List<string> { IP_BLACK_LIST }
},
new RuleConfigTimespanThreshold
{
LastCallThreshold = new System.TimeSpan(0,0, DEFAULT_TIMESPAN_SECONDS),
MaxThreshold = DEFAULT_REQUEST_THRESHOLD
}
}
};

var configNewResource = new ResourceConfig
{
ConfigId = 2,
Resource = Model.Resource.EnumResource.GetListingDetail,
Rules = new List<RuleConfigBase>
{
new RuleConfigIpWhiteList
{
IpAddresses = new List<string> { IP_WHITE_LIST }
}
}
};

var configResourceMaintenance = new ResourceConfig
{
ConfigId = 3,
Resource = Model.Resource.EnumResource.CreateListingInquiry,
Rules = new List<RuleConfigBase>
{
new RuleConfigEnabledResource
{
Enabled = false
}
}
};

repo.CreateResourceConfig(configDefault);
repo.CreateResourceConfig(configNewResource);
repo.CreateResourceConfig(configResourceMaintenance);
}

public static void OutputResponse(object response)
{
if (response == null)
response = "response is emtpy";

var lineBreak = System.Environment.NewLine;
var seperator = $"{lineBreak}##############################################################################{lineBreak}";

Debug.WriteLine(seperator);
Debug.WriteLine(JsonSerializer.Serialize(response));
Debug.WriteLine(seperator);
}
}
}
186 changes: 184 additions & 2 deletions RateLimiter.Tests/RateLimiterTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,195 @@
using NUnit.Framework;
using RateLimiter.Model.Resource;
using RateLimiter.Model.Response;
using System.Threading;

namespace RateLimiter.Tests;

[TestFixture]
public class RateLimiterTest
{
[Test]
public void Example()
public void TestResourceGetListings()
{
Assert.That(true, Is.True);
var request = new ResourceRequest
{
IpAddress = HelperRateLimiter.IP_WHITE_LIST,
Resource = EnumResource.GetListings,
Token = HelperRateLimiter.TOKEN
};

var service = HelperRateLimiter.GetServiceRateLimiter();
var response = service.Validate(request);

Assert.That(response.Success, Is.True);
}

[Test]
public void TestResourceGetListingsBlackList()
{
var request = new ResourceRequest
{
IpAddress = HelperRateLimiter.IP_BLACK_LIST,
Resource = EnumResource.GetListings,
Token = HelperRateLimiter.TOKEN
};

var service = HelperRateLimiter.GetServiceRateLimiter();
var response = service.Validate(request);

Assert.That(response.Success, Is.False);
HelperRateLimiter.OutputResponse(service.GetRequestLogAll());
}

[Test]
public void TestResourceGetListingsTimespan()
{
var request = new ResourceRequest
{
IpAddress = HelperRateLimiter.IP_WHITE_LIST,
Resource = EnumResource.GetListings,
Token = HelperRateLimiter.TOKEN
};

var service = HelperRateLimiter.GetServiceRateLimiter();

ResponseRateLimitValidation response = null;

for (int i = 0; i < HelperRateLimiter.DEFAULT_REQUEST_THRESHOLD; i++)
response = service.Validate(request);

Assert.That(response.Success, Is.True);
HelperRateLimiter.OutputResponse(service.GetRequestLogAll());
}

[Test]
public void TestResourceGetListingsTimespanExceeded()
{
var request = new ResourceRequest
{
IpAddress = HelperRateLimiter.IP_WHITE_LIST,
Resource = EnumResource.GetListings,
Token = HelperRateLimiter.TOKEN
};

var service = HelperRateLimiter.GetServiceRateLimiter();

ResponseRateLimitValidation response = null;

for (int i = 0; i < HelperRateLimiter.DEFAULT_REQUEST_THRESHOLD + 5; i++)
response = service.Validate(request);

Assert.That(response.Success, Is.False);
HelperRateLimiter.OutputResponse(service.GetRequestLogAll());
}

[Test]
public void TestResourceGetListingsTimespanEU()
{
var request = new ResourceRequest
{
IpAddress = HelperRateLimiter.IP_WHITE_LIST,
Resource = EnumResource.GetListings,
Token = HelperRateLimiter.EU_TOKEN
};

var service = HelperRateLimiter.GetServiceRateLimiter();
var delay = (HelperRateLimiter.DEFAULT_TIMESPAN_SECONDS + 2) * 1000;

ResponseRateLimitValidation response = null;

for (int i = 0; i < HelperRateLimiter.DEFAULT_REQUEST_THRESHOLD; i++)
{
Thread.Sleep(delay);
response = service.Validate(request);
Assert.That(response.Success, Is.True);
}

HelperRateLimiter.OutputResponse(service.GetRequestLogAll());
}

[Test]
public void TestResourceGetListingsTimespanExceededEU()
{
var request = new ResourceRequest
{
IpAddress = HelperRateLimiter.IP_WHITE_LIST,
Resource = EnumResource.GetListings,
Token = HelperRateLimiter.EU_TOKEN
};

var service = HelperRateLimiter.GetServiceRateLimiter();

var response = service.Validate(request);
Assert.That(response.Success, Is.True);

//back to back so we should brick here
response = service.Validate(request);
Assert.That(response.Success, Is.False);

HelperRateLimiter.OutputResponse(service.GetRequestLogAll());
}

[Test]
public void TestResourceGetListingDetail()
{
var request = new ResourceRequest
{
IpAddress = HelperRateLimiter.IP_WHITE_LIST,
Resource = EnumResource.GetListingDetail,
Token = HelperRateLimiter.TOKEN
};

var service = HelperRateLimiter.GetServiceRateLimiter();
var response = service.Validate(request);

Assert.That(response.Success, Is.True);

//swap to a non whitelisted ip
request.IpAddress = "123.55.123";

response = service.Validate(request);

Assert.That(response.Success, Is.False);

HelperRateLimiter.OutputResponse(service.GetRequestLogAll());
}

[Test]
public void TestResourceCreateListingInquiry()
{
var request = new ResourceRequest
{
IpAddress = HelperRateLimiter.IP_WHITE_LIST,
Resource = EnumResource.CreateListingInquiry,
Token = HelperRateLimiter.TOKEN
};

var service = HelperRateLimiter.GetServiceRateLimiter();
var response = service.Validate(request);

//disabled
Assert.That(response.Success, Is.False);

HelperRateLimiter.OutputResponse(service.GetRequestLogAll());
}

[Test]
public void TestResourceCreateListingFavorite()
{
var request = new ResourceRequest
{
IpAddress = HelperRateLimiter.IP_WHITE_LIST,
Resource = EnumResource.CreateListingFavorite,
Token = HelperRateLimiter.TOKEN
};

var service = HelperRateLimiter.GetServiceRateLimiter();
var response = service.Validate(request);

//no configuration
Assert.That(response.Success, Is.False);

HelperRateLimiter.OutputResponse(service.GetRequestLogAll());
}
}
19 changes: 19 additions & 0 deletions RateLimiter/Data/IRepositoryRateLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using RateLimiter.Model.Data;
using RateLimiter.Model.Resource;
using RateLimiter.Model.Resource.Config;
using RateLimiter.Model.Response;
using System;
using System.Collections.Generic;

namespace RateLimiter.Data
{
public interface IRepositoryRateLimiter
{
ResourceConfig GetResourceConfig(EnumResource resource);
int GetResourceRequestCount(EnumResource resource, string token, TimeSpan lookbackTimespan);
DateTime GetLastResourceRequest(EnumResource resource, string token);
void CreateResourceConfig(ResourceConfig resourceConfig);
void LogResourceRequestAttempt(ResourceRequest request, ResponseRateLimitValidation response);
List<ResourceRequestLog> GetRequestLogAll();
}
}
Loading