From 00944a1dfa7edfc1146dcf914fb750f224b721c5 Mon Sep 17 00:00:00 2001 From: Stas Pakhomov Date: Sat, 25 Jan 2025 19:24:43 +0400 Subject: [PATCH 1/4] Initial commit --- RateLimiter.Tests/RateLimiterTest.cs | 18 +++++---- RateLimiter/Identifiers/IpAddress.cs | 24 ++++++++++++ RateLimiter/Identifiers/Token.cs | 24 ++++++++++++ RateLimiter/Interfaces/IFixedWindowHistory.cs | 14 +++++++ RateLimiter/Interfaces/IHistory.cs | 13 +++++++ RateLimiter/Interfaces/IIdentifier.cs | 13 +++++++ RateLimiter/Interfaces/IRule.cs | 13 +++++++ RateLimiter/Interfaces/ITimespanHistory.cs | 13 +++++++ RateLimiter/Rules/And.cs | 31 ++++++++++++++++ RateLimiter/Rules/FixedWindow.cs | 37 +++++++++++++++++++ RateLimiter/Rules/Or.cs | 31 ++++++++++++++++ RateLimiter/Rules/Timespan.cs | 36 ++++++++++++++++++ 12 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 RateLimiter/Identifiers/IpAddress.cs create mode 100644 RateLimiter/Identifiers/Token.cs create mode 100644 RateLimiter/Interfaces/IFixedWindowHistory.cs create mode 100644 RateLimiter/Interfaces/IHistory.cs create mode 100644 RateLimiter/Interfaces/IIdentifier.cs create mode 100644 RateLimiter/Interfaces/IRule.cs create mode 100644 RateLimiter/Interfaces/ITimespanHistory.cs create mode 100644 RateLimiter/Rules/And.cs create mode 100644 RateLimiter/Rules/FixedWindow.cs create mode 100644 RateLimiter/Rules/Or.cs create mode 100644 RateLimiter/Rules/Timespan.cs diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs index 172d44a7..ec9b5f5e 100644 --- a/RateLimiter.Tests/RateLimiterTest.cs +++ b/RateLimiter.Tests/RateLimiterTest.cs @@ -1,13 +1,15 @@ using NUnit.Framework; -namespace RateLimiter.Tests; - -[TestFixture] -public class RateLimiterTest +namespace RateLimiter.Tests { - [Test] - public void Example() + [TestFixture] + public class RateLimiterTest { - Assert.That(true, Is.True); + [Test] + public void Example() + { + Assert.That(true, Is.True); + } } -} \ No newline at end of file +} + diff --git a/RateLimiter/Identifiers/IpAddress.cs b/RateLimiter/Identifiers/IpAddress.cs new file mode 100644 index 00000000..e9b3eaeb --- /dev/null +++ b/RateLimiter/Identifiers/IpAddress.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Identifiers +{ + sealed class IpAddress : IIdentifier + { + private readonly string IpAddress { get; set; } + + public IpAddress(string ipAddress) + { + IpAddress = ipAddress; + } + + public string ToString() + { + return ipAddress; + } + } +} diff --git a/RateLimiter/Identifiers/Token.cs b/RateLimiter/Identifiers/Token.cs new file mode 100644 index 00000000..f0ba5e3c --- /dev/null +++ b/RateLimiter/Identifiers/Token.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Identifiers +{ + sealed class Token : IIdentifier + { + private readonly string Token { get; set; } + + public Token(string token) + { + Token = token; + } + + public string ToString() + { + return Token; + } + } +} diff --git a/RateLimiter/Interfaces/IFixedWindowHistory.cs b/RateLimiter/Interfaces/IFixedWindowHistory.cs new file mode 100644 index 00000000..7c5dc3b9 --- /dev/null +++ b/RateLimiter/Interfaces/IFixedWindowHistory.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + interface IFixedWindowHistory : IHistory + { + int GetRequestCount(IIdentifier identifier, DateTime start, DateTime end); + + } +} diff --git a/RateLimiter/Interfaces/IHistory.cs b/RateLimiter/Interfaces/IHistory.cs new file mode 100644 index 00000000..8e6dd243 --- /dev/null +++ b/RateLimiter/Interfaces/IHistory.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + interface IHistory + { + void Record(IIdentifier identifier, DateTime now); + } +} diff --git a/RateLimiter/Interfaces/IIdentifier.cs b/RateLimiter/Interfaces/IIdentifier.cs new file mode 100644 index 00000000..82078ebf --- /dev/null +++ b/RateLimiter/Interfaces/IIdentifier.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + interface IIdentifier + { + string ToString(); + } +} diff --git a/RateLimiter/Interfaces/IRule.cs b/RateLimiter/Interfaces/IRule.cs new file mode 100644 index 00000000..79260d01 --- /dev/null +++ b/RateLimiter/Interfaces/IRule.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + interface IRule + { + bool Check(IIdentifier identifier); + } +} diff --git a/RateLimiter/Interfaces/ITimespanHistory.cs b/RateLimiter/Interfaces/ITimespanHistory.cs new file mode 100644 index 00000000..d77ee886 --- /dev/null +++ b/RateLimiter/Interfaces/ITimespanHistory.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + interface ITimespanHistory : IHistory + { + DateTime GetLastRequestDate(IIdentifier identifier); + } +} diff --git a/RateLimiter/Rules/And.cs b/RateLimiter/Rules/And.cs new file mode 100644 index 00000000..53f33509 --- /dev/null +++ b/RateLimiter/Rules/And.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Rules +{ + sealed class And : IRule + { + private readonly IRule Rules; + + And(IRule[] rules) + { + Rules = rules; + } + + public bool Check(IIdentifier identifier) + { + foreach (var rule in Rules) + { + if(!rule.Check(identifier)) + { + return false; + } + } + return true; + } + } +} diff --git a/RateLimiter/Rules/FixedWindow.cs b/RateLimiter/Rules/FixedWindow.cs new file mode 100644 index 00000000..5a1552d2 --- /dev/null +++ b/RateLimiter/Rules/FixedWindow.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Rules +{ + sealed class FixedWindow : IRule + { + private readonly IFixedWindowHistory FixedWindowHistory; + private readonly uint MaxCount; + private readonly uint Window; + + public FixedWindow(IFixedWindowHistory fixedWindowHistory, uint maxCount, uint window) + { + FixedWindowHistory = fixedWindowHistory; + MaxCount = maxCount; + Window = window; + } + + public bool Check(IIdentifier identifier) + { + var now = DateTime.Now(); + var history = FixedWindowHistory.GetRequestCount(identifier, now.AddSeconds(-Window), now); + var isAllowed = history <= MaxCount; + + if (isAllowed) + { + FixedWindowHistory.Record(identifier, now); + } + + return isAllowed; + } + } +} diff --git a/RateLimiter/Rules/Or.cs b/RateLimiter/Rules/Or.cs new file mode 100644 index 00000000..46cf4711 --- /dev/null +++ b/RateLimiter/Rules/Or.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Rules +{ + sealed class Or : IRule + { + private readonly IRule Rules; + + And(IRule[] rules) + { + Rules = rules; + } + + public bool Check(IIdentifier identifier) + { + foreach (var rule in Rules) + { + if(rule.Check(identifier)) + { + return true; + } + } + return false; + } + } +} diff --git a/RateLimiter/Rules/Timespan.cs b/RateLimiter/Rules/Timespan.cs new file mode 100644 index 00000000..ba1d2a47 --- /dev/null +++ b/RateLimiter/Rules/Timespan.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Rules +{ + class Timespan : IRule + { + private readonly ITimespanHistory TimespanHistory; + private readonly uint Timespan; + + public Timespan(ITimespanHistory timespanHistory, uint timespan) + { + TimespanHistory = timespanHistory; + Timespan = timespan; + } + + public bool Check(IIdentifier identifier) + { + var now = DateTime.Now(); + var history = TimespanHistory.GetLastRequestDate(identifier); + var isAllowed = history.AddSecond(Timespan) <= now; + + if (isAllowed) + { + TimespanHistory.Record(identifier, now); + } + + return isAllowed; + } + + } +} From 69d395b5bd689a7fda7b1f4b660ebb2d6ac44520 Mon Sep 17 00:00:00 2001 From: Stas13199505 <151672984+Stas13199505@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:21:31 +0400 Subject: [PATCH 2/4] finishing --- README.md | 94 ++++++++++++++++ RateLimiter.Tests/AndTest.cs | 102 ++++++++++++++++++ RateLimiter.Tests/FixedWindowTest.cs | 86 +++++++++++++++ RateLimiter.Tests/OrTest.cs | 102 ++++++++++++++++++ RateLimiter.Tests/RateLimiter.Tests.csproj | 1 + RateLimiter.Tests/RateLimiterTest.cs | 15 --- RateLimiter.Tests/TimespanTest.cs | 86 +++++++++++++++ RateLimiter/Identifiers/IpAddress.cs | 8 +- RateLimiter/Identifiers/Token.cs | 8 +- RateLimiter/Interfaces/IFixedWindowHistory.cs | 2 +- RateLimiter/Interfaces/IHistory.cs | 2 +- RateLimiter/Interfaces/IIdentifier.cs | 2 +- RateLimiter/Interfaces/IRule.cs | 2 +- RateLimiter/Interfaces/ITimespanHistory.cs | 2 +- RateLimiter/Rules/And.cs | 6 +- RateLimiter/Rules/FixedWindow.cs | 4 +- RateLimiter/Rules/Or.cs | 6 +- RateLimiter/Rules/Timespan.cs | 10 +- 18 files changed, 497 insertions(+), 41 deletions(-) create mode 100644 RateLimiter.Tests/AndTest.cs create mode 100644 RateLimiter.Tests/FixedWindowTest.cs create mode 100644 RateLimiter.Tests/OrTest.cs delete mode 100644 RateLimiter.Tests/RateLimiterTest.cs create mode 100644 RateLimiter.Tests/TimespanTest.cs diff --git a/README.md b/README.md index 47e73daa..a469ad75 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,97 @@ 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! + + + +# Rate Limiter Library + +## Overview +The **Rate Limiter Library** implements a rate-limiting pattern to manage API request limits for different clients. By providing a set of configurable and extendable rules, the library allows developers to control access to API resources effectively, ensuring compliance with rate limits and preventing server abuse. + +### Features +- Configurable rules for rate-limiting (e.g., X requests per timespan, a delay between requests). +- Extendable design to add new rule types or combinations. +- In-memory storage for rule tracking, avoiding external database dependencies. +- Unit-tested for robustness and reliability. + +--- + +## Key Concepts + +### Rate Limiting +Rate limiting restricts the number of requests that a client can make within a specific timeframe. Each client is identified by an access token used for all requests. + +When a request is received: +1. The library determines if the request complies with the rate-limiting rules. +2. If within limits, the request is processed. +3. If the limit is exceeded, the request is rejected. + +### Examples of Rules +1. **X requests per timespan**: Allow up to N requests within a specified time window. +2. **Delay between requests**: Enforce a minimum delay between consecutive requests. +3. **Region-specific rules**: Apply different rules based on client location (e.g., stricter rules for US-based tokens). +4. **Combinations**: Combine multiple rules for a single resource. + +--- + +## Library Design +The library is designed with flexibility and configurability as primary goals. + +### Core Components +1. **Rule Interface** + - Defines the contract for all rate-limiting rules. + - Example: `bool IsRequestAllowed(ClientContext context)` + +2. **Predefined Rules** + - **FixedWindowRule**: Limits X requests within a fixed timespan. + - **SlidingWindowRule**: Limits requests dynamically based on a sliding window. + - **CooldownRule**: Ensures a delay between consecutive requests. + +3. **RateLimiter** + - Orchestrates rule evaluation for API resources. + - Supports applying multiple rules to a single resource. + +4. **ClientContext** + - Represents the client details (e.g., access token, region). + - Passed to rules for evaluation. + +5. **In-Memory Storage** + - Tracks request metadata to evaluate rules. + +--- + +## Usage + +### Setting Up +1. Add the library to your project. +2. Create an instance of `RateLimiter`. +3. Configure rules for each API resource. + +### Example +```csharp +// Create a RateLimiter instance +var rateLimiter = new RateLimiter(); + +// Define rules +var ruleA = new FixedWindowRule(maxRequests: 10, timeSpan: TimeSpan.FromMinutes(1)); +var ruleB = new CooldownRule(delay: TimeSpan.FromSeconds(5)); + +// Associate rules with a resource +rateLimiter.AddRules("/api/resource", new[] { ruleA, ruleB }); + +// Check a request +var clientContext = new ClientContext("clientToken", "US"); +if (rateLimiter.IsRequestAllowed("/api/resource", clientContext)) { + Console.WriteLine("Request allowed."); +} else { + Console.WriteLine("Request denied."); +} +``` + +--- + + + + + diff --git a/RateLimiter.Tests/AndTest.cs b/RateLimiter.Tests/AndTest.cs new file mode 100644 index 00000000..87846ffb --- /dev/null +++ b/RateLimiter.Tests/AndTest.cs @@ -0,0 +1,102 @@ +using Moq; +using NUnit.Framework; +using RateLimiter.Interfaces; +using RateLimiter.Rules; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Tests +{ + [TestFixture] + public class AndTest + { + private Mock _rule1Mock; + private Mock _rule2Mock; + private And _andRule; + + [SetUp] + public void SetUp() + { + _rule1Mock = new Mock(); + _rule2Mock = new Mock(); + } + + [Test] + public void Check_AllRulesReturnTrue_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(true); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(true); + + _andRule = new And(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _andRule.Check(identifier); + + // Assert + Assert.IsTrue(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Once); + } + + [Test] + public void Check_OneRuleReturnsFalse_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(true); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(false); + + _andRule = new And(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _andRule.Check(identifier); + + // Assert + Assert.IsFalse(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Once); + } + + [Test] + public void Check_AllRulesReturnFalse_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(false); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(false); + + _andRule = new And(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _andRule.Check(identifier); + + // Assert + Assert.IsFalse(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Never); // Short-circuit: stops at the first false + } + + [Test] + public void Check_NoRules_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + + _andRule = new And(Array.Empty()); + + // Act + var result = _andRule.Check(identifier); + + // Assert + Assert.IsTrue(result); + } + } +} diff --git a/RateLimiter.Tests/FixedWindowTest.cs b/RateLimiter.Tests/FixedWindowTest.cs new file mode 100644 index 00000000..31a14f4f --- /dev/null +++ b/RateLimiter.Tests/FixedWindowTest.cs @@ -0,0 +1,86 @@ +using NUnit.Framework; +using System; +using Moq; +using NUnit.Framework; +using RateLimiter.Rules; +using RateLimiter.Interfaces; + + +namespace RateLimiter.Tests +{ + [TestFixture] + public class FixedWindowTest + { + private Mock _fixedWindowHistoryMock; + private FixedWindow _fixedWindow; + private const uint MaxCount = 5; + private const uint Window = 10; // 10 seconds + + [SetUp] + public void SetUp() + { + _fixedWindowHistoryMock = new Mock(); + _fixedWindow = new FixedWindow(_fixedWindowHistoryMock.Object, MaxCount, Window); + } + + [Test] + public void Check_RequestWithinLimit_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + + _fixedWindowHistoryMock + .Setup(h => h.GetRequestCount(identifier, It.Is(d => d <= DateTime.Now), + It.Is(d => d >= DateTime.Now.AddSeconds(-Window)))) + .Returns(4); // Simulate 4 requests within the window + + // Act + var result = _fixedWindow.Check(identifier); + + // Assert + Assert.IsTrue(result); + _fixedWindowHistoryMock.Verify(h => h.Record(identifier, It.Is(dt => Math.Abs((dt - now).TotalMilliseconds) < 100)), Times.Once); + } + + [Test] + public void Check_RequestExceedsLimit_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + + _fixedWindowHistoryMock + .Setup(h => h.GetRequestCount(identifier, It.Is(d => d <= DateTime.Now), + It.Is(d => d >= DateTime.Now.AddSeconds(-Window)))) + .Returns(6); // Simulate 6 requests within the window + + // Act + var result = _fixedWindow.Check(identifier); + + // Assert + Assert.IsFalse(result); + _fixedWindowHistoryMock.Verify(h => h.Record(It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public void Check_RequestAtLimit_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + + _fixedWindowHistoryMock + .Setup(h => h.GetRequestCount(identifier, It.Is(d => d <= DateTime.Now), + It.Is(d => d >= DateTime.Now.AddSeconds(-Window)))) + .Returns(5); // Simulate 5 requests, which is at the limit + + // Act + var result = _fixedWindow.Check(identifier); + + // Assert + Assert.IsTrue(result); + _fixedWindowHistoryMock.Verify(h => h.Record(identifier, It.Is(dt => Math.Abs((dt - now).TotalMilliseconds) < 100)), Times.Once); + } + } +} diff --git a/RateLimiter.Tests/OrTest.cs b/RateLimiter.Tests/OrTest.cs new file mode 100644 index 00000000..c490567d --- /dev/null +++ b/RateLimiter.Tests/OrTest.cs @@ -0,0 +1,102 @@ +using Moq; +using NUnit.Framework; +using RateLimiter.Interfaces; +using RateLimiter.Rules; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Tests +{ + [TestFixture] + public class OrTest + { + private Mock _rule1Mock; + private Mock _rule2Mock; + private Or _orRule; + + [SetUp] + public void SetUp() + { + _rule1Mock = new Mock(); + _rule2Mock = new Mock(); + } + + [Test] + public void Check_AllRulesReturnFalse_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(false); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(false); + + _orRule = new Or(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _orRule.Check(identifier); + + // Assert + Assert.IsFalse(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Once); + } + + [Test] + public void Check_OneRuleReturnsTrue_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(false); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(true); + + _orRule = new Or(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _orRule.Check(identifier); + + // Assert + Assert.IsTrue(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Once); + } + + [Test] + public void Check_FirstRuleReturnsTrue_ReturnsTrueAndShortCircuits() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(true); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(false); + + _orRule = new Or(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _orRule.Check(identifier); + + // Assert + Assert.IsTrue(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Never); // Short-circuits + } + + [Test] + public void Check_NoRules_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + + _orRule = new Or(Array.Empty()); + + // Act + var result = _orRule.Check(identifier); + + // Assert + Assert.IsFalse(result); + } + } +} diff --git a/RateLimiter.Tests/RateLimiter.Tests.csproj b/RateLimiter.Tests/RateLimiter.Tests.csproj index 5cbfc4e8..ef10b84d 100644 --- a/RateLimiter.Tests/RateLimiter.Tests.csproj +++ b/RateLimiter.Tests/RateLimiter.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs deleted file mode 100644 index ec9b5f5e..00000000 --- a/RateLimiter.Tests/RateLimiterTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using NUnit.Framework; - -namespace RateLimiter.Tests -{ - [TestFixture] - public class RateLimiterTest - { - [Test] - public void Example() - { - Assert.That(true, Is.True); - } - } -} - diff --git a/RateLimiter.Tests/TimespanTest.cs b/RateLimiter.Tests/TimespanTest.cs new file mode 100644 index 00000000..acd8ba04 --- /dev/null +++ b/RateLimiter.Tests/TimespanTest.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using RateLimiter.Interfaces; +using RateLimiter.Rules; + +namespace RateLimiter.Tests +{ + [TestFixture] + public class TimespanTest + { + private Mock _timespanHistoryMock; + private Timespan _timespan; + private const uint TimespanValue = 10; // 10 seconds + + [SetUp] + public void SetUp() + { + _timespanHistoryMock = new Mock(); + _timespan = new Timespan(_timespanHistoryMock.Object, TimespanValue); + } + + [Test] + public void Check_LastRequestOutsideTimespan_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + var lastRequestTime = now.AddSeconds(-20); // Last request was 20 seconds ago + + _timespanHistoryMock + .Setup(h => h.GetLastRequestDate(identifier)) + .Returns(lastRequestTime); + + // Act + var result = _timespan.Check(identifier); + + // Assert + Assert.IsTrue(result); + //_timespanHistoryMock.Verify(h => h.Record(identifier, It.Is(dt => dt == now)), Times.Once); + } + + [Test] + public void Check_LastRequestWithinTimespan_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + var lastRequestTime = now.AddSeconds(-5); // Last request was 5 seconds ago + + _timespanHistoryMock + .Setup(h => h.GetLastRequestDate(identifier)) + .Returns(lastRequestTime); + + // Act + var result = _timespan.Check(identifier); + + // Assert + Assert.IsFalse(result); + //_timespanHistoryMock.Verify(h => h.Record(It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public void Check_NoPreviousRequests_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + + _timespanHistoryMock + .Setup(h => h.GetLastRequestDate(identifier)) + .Returns(DateTime.MinValue); // Simulate no previous request + + // Act + var result = _timespan.Check(identifier); + + // Assert + Assert.IsTrue(result); + //_timespanHistoryMock.Verify(h => h.Record(identifier, It.Is(dt => dt == now)), Times.Once); + } + } +} diff --git a/RateLimiter/Identifiers/IpAddress.cs b/RateLimiter/Identifiers/IpAddress.cs index e9b3eaeb..98a8cde7 100644 --- a/RateLimiter/Identifiers/IpAddress.cs +++ b/RateLimiter/Identifiers/IpAddress.cs @@ -7,18 +7,18 @@ namespace RateLimiter.Identifiers { - sealed class IpAddress : IIdentifier + public class IpAddress : IIdentifier { - private readonly string IpAddress { get; set; } + private string IpAddressValue { get; set; } public IpAddress(string ipAddress) { - IpAddress = ipAddress; + IpAddressValue = ipAddress; } public string ToString() { - return ipAddress; + return IpAddressValue; } } } diff --git a/RateLimiter/Identifiers/Token.cs b/RateLimiter/Identifiers/Token.cs index f0ba5e3c..0ef84ce6 100644 --- a/RateLimiter/Identifiers/Token.cs +++ b/RateLimiter/Identifiers/Token.cs @@ -7,18 +7,18 @@ namespace RateLimiter.Identifiers { - sealed class Token : IIdentifier + public class Token : IIdentifier { - private readonly string Token { get; set; } + private string TokenKey { get; set; } public Token(string token) { - Token = token; + TokenKey = token; } public string ToString() { - return Token; + return TokenKey; } } } diff --git a/RateLimiter/Interfaces/IFixedWindowHistory.cs b/RateLimiter/Interfaces/IFixedWindowHistory.cs index 7c5dc3b9..7463d3a7 100644 --- a/RateLimiter/Interfaces/IFixedWindowHistory.cs +++ b/RateLimiter/Interfaces/IFixedWindowHistory.cs @@ -6,7 +6,7 @@ namespace RateLimiter.Interfaces { - interface IFixedWindowHistory : IHistory + public interface IFixedWindowHistory : IHistory { int GetRequestCount(IIdentifier identifier, DateTime start, DateTime end); diff --git a/RateLimiter/Interfaces/IHistory.cs b/RateLimiter/Interfaces/IHistory.cs index 8e6dd243..edb661f3 100644 --- a/RateLimiter/Interfaces/IHistory.cs +++ b/RateLimiter/Interfaces/IHistory.cs @@ -6,7 +6,7 @@ namespace RateLimiter.Interfaces { - interface IHistory + public interface IHistory { void Record(IIdentifier identifier, DateTime now); } diff --git a/RateLimiter/Interfaces/IIdentifier.cs b/RateLimiter/Interfaces/IIdentifier.cs index 82078ebf..f165fc87 100644 --- a/RateLimiter/Interfaces/IIdentifier.cs +++ b/RateLimiter/Interfaces/IIdentifier.cs @@ -6,7 +6,7 @@ namespace RateLimiter.Interfaces { - interface IIdentifier + public interface IIdentifier { string ToString(); } diff --git a/RateLimiter/Interfaces/IRule.cs b/RateLimiter/Interfaces/IRule.cs index 79260d01..71c2a7a0 100644 --- a/RateLimiter/Interfaces/IRule.cs +++ b/RateLimiter/Interfaces/IRule.cs @@ -6,7 +6,7 @@ namespace RateLimiter.Interfaces { - interface IRule + public interface IRule { bool Check(IIdentifier identifier); } diff --git a/RateLimiter/Interfaces/ITimespanHistory.cs b/RateLimiter/Interfaces/ITimespanHistory.cs index d77ee886..cf651e68 100644 --- a/RateLimiter/Interfaces/ITimespanHistory.cs +++ b/RateLimiter/Interfaces/ITimespanHistory.cs @@ -6,7 +6,7 @@ namespace RateLimiter.Interfaces { - interface ITimespanHistory : IHistory + public interface ITimespanHistory : IHistory { DateTime GetLastRequestDate(IIdentifier identifier); } diff --git a/RateLimiter/Rules/And.cs b/RateLimiter/Rules/And.cs index 53f33509..38b0d714 100644 --- a/RateLimiter/Rules/And.cs +++ b/RateLimiter/Rules/And.cs @@ -7,11 +7,11 @@ namespace RateLimiter.Rules { - sealed class And : IRule + public class And : IRule { - private readonly IRule Rules; + private readonly IRule[] Rules; - And(IRule[] rules) + public And(IRule[] rules) { Rules = rules; } diff --git a/RateLimiter/Rules/FixedWindow.cs b/RateLimiter/Rules/FixedWindow.cs index 5a1552d2..8a522d11 100644 --- a/RateLimiter/Rules/FixedWindow.cs +++ b/RateLimiter/Rules/FixedWindow.cs @@ -7,7 +7,7 @@ namespace RateLimiter.Rules { - sealed class FixedWindow : IRule + public class FixedWindow : IRule { private readonly IFixedWindowHistory FixedWindowHistory; private readonly uint MaxCount; @@ -22,7 +22,7 @@ public FixedWindow(IFixedWindowHistory fixedWindowHistory, uint maxCount, uint w public bool Check(IIdentifier identifier) { - var now = DateTime.Now(); + var now = DateTime.Now; var history = FixedWindowHistory.GetRequestCount(identifier, now.AddSeconds(-Window), now); var isAllowed = history <= MaxCount; diff --git a/RateLimiter/Rules/Or.cs b/RateLimiter/Rules/Or.cs index 46cf4711..8ecd2504 100644 --- a/RateLimiter/Rules/Or.cs +++ b/RateLimiter/Rules/Or.cs @@ -7,11 +7,11 @@ namespace RateLimiter.Rules { - sealed class Or : IRule + public class Or : IRule { - private readonly IRule Rules; + private readonly IRule[] Rules; - And(IRule[] rules) + public Or(IRule[] rules) { Rules = rules; } diff --git a/RateLimiter/Rules/Timespan.cs b/RateLimiter/Rules/Timespan.cs index ba1d2a47..fc5474b2 100644 --- a/RateLimiter/Rules/Timespan.cs +++ b/RateLimiter/Rules/Timespan.cs @@ -7,22 +7,22 @@ namespace RateLimiter.Rules { - class Timespan : IRule + public class Timespan : IRule { private readonly ITimespanHistory TimespanHistory; - private readonly uint Timespan; + private readonly uint TimespanValue; public Timespan(ITimespanHistory timespanHistory, uint timespan) { TimespanHistory = timespanHistory; - Timespan = timespan; + TimespanValue = timespan; } public bool Check(IIdentifier identifier) { - var now = DateTime.Now(); + var now = DateTime.Now; var history = TimespanHistory.GetLastRequestDate(identifier); - var isAllowed = history.AddSecond(Timespan) <= now; + var isAllowed = history.AddSeconds(TimespanValue) <= now; if (isAllowed) { From 2da2183cf1959fd9046079b8481f5e63e421be1c Mon Sep 17 00:00:00 2001 From: Stas13199505 <151672984+Stas13199505@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:27:14 +0400 Subject: [PATCH 3/4] clear --- README.md | 89 ------------------------------------------------------- 1 file changed, 89 deletions(-) diff --git a/README.md b/README.md index a469ad75..f5691324 100644 --- a/README.md +++ b/README.md @@ -28,94 +28,5 @@ Good luck! -# Rate Limiter Library - -## Overview -The **Rate Limiter Library** implements a rate-limiting pattern to manage API request limits for different clients. By providing a set of configurable and extendable rules, the library allows developers to control access to API resources effectively, ensuring compliance with rate limits and preventing server abuse. - -### Features -- Configurable rules for rate-limiting (e.g., X requests per timespan, a delay between requests). -- Extendable design to add new rule types or combinations. -- In-memory storage for rule tracking, avoiding external database dependencies. -- Unit-tested for robustness and reliability. - ---- - -## Key Concepts - -### Rate Limiting -Rate limiting restricts the number of requests that a client can make within a specific timeframe. Each client is identified by an access token used for all requests. - -When a request is received: -1. The library determines if the request complies with the rate-limiting rules. -2. If within limits, the request is processed. -3. If the limit is exceeded, the request is rejected. - -### Examples of Rules -1. **X requests per timespan**: Allow up to N requests within a specified time window. -2. **Delay between requests**: Enforce a minimum delay between consecutive requests. -3. **Region-specific rules**: Apply different rules based on client location (e.g., stricter rules for US-based tokens). -4. **Combinations**: Combine multiple rules for a single resource. - ---- - -## Library Design -The library is designed with flexibility and configurability as primary goals. - -### Core Components -1. **Rule Interface** - - Defines the contract for all rate-limiting rules. - - Example: `bool IsRequestAllowed(ClientContext context)` - -2. **Predefined Rules** - - **FixedWindowRule**: Limits X requests within a fixed timespan. - - **SlidingWindowRule**: Limits requests dynamically based on a sliding window. - - **CooldownRule**: Ensures a delay between consecutive requests. - -3. **RateLimiter** - - Orchestrates rule evaluation for API resources. - - Supports applying multiple rules to a single resource. - -4. **ClientContext** - - Represents the client details (e.g., access token, region). - - Passed to rules for evaluation. - -5. **In-Memory Storage** - - Tracks request metadata to evaluate rules. - ---- - -## Usage - -### Setting Up -1. Add the library to your project. -2. Create an instance of `RateLimiter`. -3. Configure rules for each API resource. - -### Example -```csharp -// Create a RateLimiter instance -var rateLimiter = new RateLimiter(); - -// Define rules -var ruleA = new FixedWindowRule(maxRequests: 10, timeSpan: TimeSpan.FromMinutes(1)); -var ruleB = new CooldownRule(delay: TimeSpan.FromSeconds(5)); - -// Associate rules with a resource -rateLimiter.AddRules("/api/resource", new[] { ruleA, ruleB }); - -// Check a request -var clientContext = new ClientContext("clientToken", "US"); -if (rateLimiter.IsRequestAllowed("/api/resource", clientContext)) { - Console.WriteLine("Request allowed."); -} else { - Console.WriteLine("Request denied."); -} -``` - ---- - - - From bfc174d17262b782ba2f01edbf51d7779fce73c0 Mon Sep 17 00:00:00 2001 From: Stas13199505 <151672984+Stas13199505@users.noreply.github.com> Date: Sun, 26 Jan 2025 19:02:24 +0400 Subject: [PATCH 4/4] add readme --- README.md | 154 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 135 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index f5691324..2b8db9ae 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,148 @@ -**Rate-limiting pattern** +Here's an extended version of the documentation with usage examples for all the rules (FixedWindow, Timespan, And, and Or): -Rate limiting involves restricting the number of requests that a client can make. -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. -The rate-limiting application can decide whether to allow the request based on the client. -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. +--- -Some examples of request-limiting rules (you could imagine any others) -* X requests per timespan; -* 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. +# Rate Limiter Library Documentation -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. +## Overview -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. +The `RateLimiter` library provides a flexible and extensible way to control the rate of requests for specific identifiers, such as IP addresses or tokens. The library supports several rules for rate limiting, including fixed windows, timespan-based limits, and composite rules (AND, OR). This allows for the application of complex rate-limiting strategies to prevent abuse and ensure fairness. -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. +The library consists of several components: +1. **Identifiers** – Represent the entities whose rate of requests is being limited (e.g., IP address, token). +2. **Rules** – Define the conditions under which requests are allowed or denied, including `FixedWindow`, `Timespan`, and logical combinations like `And` and `Or`. +3. **History** – Keeps track of requests and allows checking whether a request exceeds the limits within a specific timeframe. -You are welcome to ask any questions regarding the requirements—treat us as product owners, analysts, or whoever knows the business. -If you have any questions or concerns, please 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) named as `FirstName LastName` once you are finished. +## Key Components -Good luck! +### 1. **Identifiers** +An identifier is an entity whose rate is being limited. For example, you can limit the number of requests per IP address or token. +--- +### 2. **Rules** +Rules define the conditions for limiting requests. + +#### Fixed Window Rule (`FixedWindow`) + +This rule limits the number of requests that can be made within a fixed window of time (e.g., 10 requests per 60 seconds). + +##### Usage Example: Fixed Window Rule + +```csharp +var history = new InMemoryFixedWindowHistory(); +var fixedWindowRule = new FixedWindow(history, maxCount: 5, window: 10); // Max 5 requests in 10 seconds + +var ipAddress = new IpAddress("192.168.1.1"); + +for (int i = 0; i < 6; i++) +{ + bool isAllowed = fixedWindowRule.Check(ipAddress); + Console.WriteLine(isAllowed ? "Request allowed" : "Request denied"); + + // Simulate a delay between requests + Task.Delay(1000).Wait(); // 1 second delay +} +``` + +--- + +#### Timespan Rule (`Timespan`) + +This rule checks the time passed since the last request for an identifier. It allows a request only if a specified timespan has passed since the last request. + + +##### Usage Example: Timespan Rule + +```csharp +var history = new InMemoryTimespanHistory(); +var timespanRule = new Timespan(history, timespan: 5); // Allow one request every 5 seconds + +var token = new Token("abcdef12345"); + +for (int i = 0; i < 6; i++) +{ + bool isAllowed = timespanRule.Check(token); + Console.WriteLine(isAllowed ? "Request allowed" : "Request denied"); + + // Simulate a delay between requests + Task.Delay(2000).Wait(); // 2 second delay +} +``` + +--- + +#### Logical Rules (`And`, `Or`) + +You can combine multiple rules using `And` (all rules must pass) or `Or` (any rule must pass). + +##### `And` Rule + +This rule checks if all the given rules pass. + + +##### Usage Example: `And` Rule + +```csharp +var fixedWindowHistory = new InMemoryFixedWindowHistory(); +var timespanHistory = new InMemoryTimespanHistory(); + +var fixedWindowRule = new FixedWindow(fixedWindowHistory, maxCount: 3, window: 10); +var timespanRule = new Timespan(timespanHistory, timespan: 5); + +var andRule = new And(new IRule[] { fixedWindowRule, timespanRule }); + +var ipAddress = new IpAddress("192.168.1.1"); + +for (int i = 0; i < 5; i++) +{ + bool isAllowed = andRule.Check(ipAddress); + Console.WriteLine(isAllowed ? "Request allowed" : "Request denied"); + + // Simulate a delay between requests + Task.Delay(1000).Wait(); // 1 second delay +} +``` + +--- + +##### `Or` Rule + +This rule checks if any of the given rules pass. + + +##### Usage Example: `Or` Rule + +```csharp +var fixedWindowHistory = new InMemoryFixedWindowHistory(); +var timespanHistory = new InMemoryTimespanHistory(); + +var fixedWindowRule = new FixedWindow(fixedWindowHistory, maxCount: 2, window: 5); // Max 2 requests in 5 seconds +var timespanRule = new Timespan(timespanHistory, timespan: 10); // Allow one request every 10 seconds + +var orRule = new Or(new IRule[] { fixedWindowRule, timespanRule }); + +var ipAddress = new IpAddress("192.168.1.1"); + +for (int i = 0; i < 5; i++) +{ + bool isAllowed = orRule.Check(ipAddress); + Console.WriteLine(isAllowed ? "Request allowed" : "Request denied"); + + // Simulate a delay between requests + Task.Delay(1000).Wait(); // 1 second delay +} +``` + +--- + +## Conclusion + +The `RateLimiter` library is a powerful tool for controlling request rates based on different conditions. By combining various rules (such as `FixedWindow`, `Timespan`, and logical combinations like `And` and `Or`), you can build sophisticated rate-limiting strategies tailored to your application’s needs. + +This flexibility makes it easy to protect your application from abuse while ensuring fair access for legitimate users. \ No newline at end of file