From 73671661eb948e87c200a2b772f8ad0911ca577d Mon Sep 17 00:00:00 2001 From: Serge P Cohen Date: Mon, 15 Jul 2024 15:38:11 -0700 Subject: [PATCH] Initial checkin of a working rate limiting application that supports logical expression trees. (see unit test setup for example) - This could obviously taken further to support serialization/deserialization of rule to Json, performance optimizations, etc. Hopefully it is sufficient o demonstrate my approach in both architecture and coding style. --- .../MSTestRateLimiter.Test.csproj | 27 +++ MSTestRateLimiter.Test/UnitTest1.cs | 167 ++++++++++++++++++ RateLimiter.Tests/RateLimiter.Tests.csproj | 1 + RateLimiter.Tests/RateLimiterTest.cs | 1 + RateLimiter.Tests/UnitTest1.cs | 167 ++++++++++++++++++ RateLimiter.sln | 18 +- RateLimiter/Interface/IClient.cs | 14 ++ RateLimiter/Interface/IRequestService.cs | 9 + RateLimiter/Interface/IResourceRequest.cs | 10 ++ RateLimiter/Interface/IRuleBase.cs | 12 ++ RateLimiter/Model/ReqClient.cs | 15 ++ RateLimiter/Model/ResourceRequest.cs | 12 ++ RateLimiter/Model/Rules/MaxPerTimeSpanRule.cs | 31 ++++ .../Model/Rules/MaxTimeSpanLapsRule.cs | 33 ++++ .../Model/Rules/MinTimeSpanLapsRule.cs | 33 ++++ RateLimiter/Model/Rules/RuleList.cs | 57 ++++++ RateLimiter/Model/Rules/USBasedRequestRule.cs | 21 +++ RateLimiter/Service/RateLimitingEngine.cs | 35 ++++ RateLimiter/Service/RequestService.cs | 35 ++++ 19 files changed, 689 insertions(+), 9 deletions(-) create mode 100644 MSTestRateLimiter.Test/MSTestRateLimiter.Test.csproj create mode 100644 MSTestRateLimiter.Test/UnitTest1.cs create mode 100644 RateLimiter.Tests/UnitTest1.cs create mode 100644 RateLimiter/Interface/IClient.cs create mode 100644 RateLimiter/Interface/IRequestService.cs create mode 100644 RateLimiter/Interface/IResourceRequest.cs create mode 100644 RateLimiter/Interface/IRuleBase.cs create mode 100644 RateLimiter/Model/ReqClient.cs create mode 100644 RateLimiter/Model/ResourceRequest.cs create mode 100644 RateLimiter/Model/Rules/MaxPerTimeSpanRule.cs create mode 100644 RateLimiter/Model/Rules/MaxTimeSpanLapsRule.cs create mode 100644 RateLimiter/Model/Rules/MinTimeSpanLapsRule.cs create mode 100644 RateLimiter/Model/Rules/RuleList.cs create mode 100644 RateLimiter/Model/Rules/USBasedRequestRule.cs create mode 100644 RateLimiter/Service/RateLimitingEngine.cs create mode 100644 RateLimiter/Service/RequestService.cs diff --git a/MSTestRateLimiter.Test/MSTestRateLimiter.Test.csproj b/MSTestRateLimiter.Test/MSTestRateLimiter.Test.csproj new file mode 100644 index 00000000..2e01ecdc --- /dev/null +++ b/MSTestRateLimiter.Test/MSTestRateLimiter.Test.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/MSTestRateLimiter.Test/UnitTest1.cs b/MSTestRateLimiter.Test/UnitTest1.cs new file mode 100644 index 00000000..00b3b904 --- /dev/null +++ b/MSTestRateLimiter.Test/UnitTest1.cs @@ -0,0 +1,167 @@ + +using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading.Tasks; +using System; +using RateLimiter.Interface; +using RateLimiter.Service; +using RateLimiter.Model.Rules; +using RateLimiter.Model; + +namespace MSTestRateLimiter.Test +{ + [TestClass] + public class UnitTest1 + { + static IRequestService reqsvc = new RequestService(); + RateLimitingEngine eng = new RateLimitingEngine(reqsvc); //use dependency injection in prod project + + [TestInitialize] + public void setupEngine() + { + // Explanation of rules + //rule1: acceess "aaa" max 3 times in 5 seconds + // AND + //rule2: access no more than 10 files per second + // AND + // ( + // ( + //rule3: requestor is USBasedRequestRule + // AND + //rule4: Access "bbb" max 5 times in 2 seconds + // ) + // OR + // ( + //rule5: requestor is not USBasedRequestRule + // AND + //rule6: Access "bbb" can only access after 2 second wait + // ) + // ) + + eng.StartingRuleList.Rulelist.Add(new MaxPerTimeSpanRule() { maxcount = 3, Resource = "aaa", timespan = 5 }); + eng.StartingRuleList.Rulelist.Add(new MaxPerTimeSpanRule() { maxcount = 10, timespan = 1 }); + var secondLevelRuleList = new RuleList() { ListOperand = "Or" }; + var ThirdLevelRuleList1 = new RuleList() { ListOperand = "And" }; + var ThirdLevelRuleList2 = new RuleList() { ListOperand = "And" }; + secondLevelRuleList.Rulelist.Add(ThirdLevelRuleList1); + secondLevelRuleList.Rulelist.Add(ThirdLevelRuleList2); + + ThirdLevelRuleList1.Rulelist.Add(new USBasedRequestRule() { Resource = "bbb", TargetResult = true }); + ThirdLevelRuleList1.Rulelist.Add(new MaxPerTimeSpanRule() { Resource = "bbb", maxcount = 5, timespan = 2 }); + + ThirdLevelRuleList2.Rulelist.Add(new USBasedRequestRule() { Resource = "bbb", TargetResult = false }); + ThirdLevelRuleList2.Rulelist.Add(new MinTimeSpanLapsRule() { Resource = "bbb", Laps = 2 }); + + //eng.StartingRuleList.Rulelist.Add(secondLevelRuleList); + eng.StartingRuleList.Rulelist.Add(secondLevelRuleList); + + + } + + + [TestMethod] + public async Task Rule1Met() + { + var dt = DateTime.Now; + bool res = true; + for (int i = 0; i < 3; i++) + { + dt = dt.AddMilliseconds(10); + var Request = new ResourceRequest { DateTime = dt, Resource = "aaa" }; + res = res && await eng.Evaluate(Request, "US32fdgfdgbfdklfds"); + } + Assert.IsTrue(res); + } + [TestMethod] + public async Task Rule1NotMet() + { + var dt = DateTime.Now; + bool res = true; + for (int i = 0; i < 10; i++) + { + dt = dt.AddMilliseconds(10); + var Request = new ResourceRequest { DateTime = dt, Resource = "aaa" }; + res = res && await eng.Evaluate(Request, "US32fdgfdgbfdklfds"); + } + Assert.IsFalse(res); + } + + [TestMethod] + public async Task Rule2NotMet() + { + var dt = DateTime.Now; + string resourceString = "aaa"; + bool res = true; + for (int i = 0; i < 11; i++) + { + dt = dt.AddMilliseconds(1); + resourceString = resourceString + "a"; + var Request = new ResourceRequest { DateTime = dt, Resource = resourceString }; + res = res && await eng.Evaluate(Request, "US32fdgfdgbfdklfds"); + } + Assert.IsFalse(res); + } + + + + [TestMethod] + public async Task TestHasRules() + { + var Request = new ResourceRequest { DateTime = DateTime.Now, Resource = "aaa" }; + var client = new ReqClient("US1cliwndjaslh") { Region = "EU", Subscription = "1" }; + Assert.IsTrue(await eng.Evaluate(Request, client)); + + + } + [TestMethod] + public async Task Rule5And6Met() + { + var Request = new ResourceRequest { DateTime = DateTime.Now, Resource = "bbb" }; + await eng.Evaluate(Request, "EU2gbfdklfds"); + var NewRequest = new ResourceRequest { DateTime = Request.DateTime.AddHours(1), Resource = "bbb" }; + var res = await eng.Evaluate(NewRequest, "EU2gbfdklfds"); + Assert.IsTrue(res); + } + [TestMethod] + public async Task Rule5MetAnd6NotMet() + { + var Request = new ResourceRequest { DateTime = DateTime.Now, Resource = "bbb" }; + await eng.Evaluate(Request, "EU2gbfdklfds"); + var NewRequest = new ResourceRequest { DateTime = Request.DateTime.AddSeconds(1), Resource = "bbb" }; + var res = await eng.Evaluate(NewRequest, "EU2gbfdklfds"); + Assert.IsFalse(res); + } + + [TestMethod] + public async Task Rule3And4Met() + { + var dt = DateTime.Now; + bool res = true; + for (int i = 0; i < 3; i++) + { + dt = dt.AddMilliseconds(100); + var Request = new ResourceRequest { DateTime = dt, Resource = "bbb" }; + res = res && await eng.Evaluate(Request, "US2gbfdklfds"); + } + + Assert.IsTrue(res); + } + [TestMethod] + public async Task Rule3And4NotMet() + { + var dt = DateTime.Now; + bool res = true; + for (int i = 0; i < 10; i++) + { + dt = dt.AddMilliseconds(100); + var Request = new ResourceRequest { DateTime = dt, Resource = "bbb" }; + res = res && await eng.Evaluate(Request, "US2gbfdklfds"); + } + + Assert.IsFalse(res); + } + + + + } +} \ No newline at end of file diff --git a/RateLimiter.Tests/RateLimiter.Tests.csproj b/RateLimiter.Tests/RateLimiter.Tests.csproj index 5cbfc4e8..2a461c51 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 index 172d44a7..bd75dba8 100644 --- a/RateLimiter.Tests/RateLimiterTest.cs +++ b/RateLimiter.Tests/RateLimiterTest.cs @@ -5,6 +5,7 @@ namespace RateLimiter.Tests; [TestFixture] public class RateLimiterTest { + [Test] public void Example() { diff --git a/RateLimiter.Tests/UnitTest1.cs b/RateLimiter.Tests/UnitTest1.cs new file mode 100644 index 00000000..30c1127b --- /dev/null +++ b/RateLimiter.Tests/UnitTest1.cs @@ -0,0 +1,167 @@ +using RateLimiter.Interface; +using RateLimiter.Model; +using RateLimiter.Model.Rules; +using RateLimiter.Service; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading.Tasks; +using System; + + +namespace TestProject1 +{ + [TestClass] + public class UnitTest1 + { + static IRequestService reqsvc = new RequestService(); + RateLimitingEngine eng = new RateLimitingEngine(reqsvc); //use dependency injection in prod project + + [TestInitialize] + public void setupEngine() + { + // Explanation of rules + //rule1: acceess "aaa" max 3 times in 5 seconds + // AND + //rule2: access no more than 10 files per second + // AND + // ( + // ( + //rule3: requestor is USBasedRequestRule + // AND + //rule4: Access "bbb" max 5 times in 2 seconds + // ) + // OR + // ( + //rule5: requestor is not USBasedRequestRule + // AND + //rule6: Access "bbb" can only access after 2 second wait + // ) + // ) + + eng.StartingRuleList.Rulelist.Add(new MaxPerTimeSpanRule() { maxcount = 3, Resource = "aaa", timespan = 5 }); + eng.StartingRuleList.Rulelist.Add(new MaxPerTimeSpanRule() { maxcount = 10, timespan = 1 }); + var secondLevelRuleList = new RuleList() { ListOperand = "Or" }; + var ThirdLevelRuleList1 = new RuleList() { ListOperand = "And" }; + var ThirdLevelRuleList2 = new RuleList() { ListOperand = "And" }; + secondLevelRuleList.Rulelist.Add(ThirdLevelRuleList1); + secondLevelRuleList.Rulelist.Add(ThirdLevelRuleList2); + + ThirdLevelRuleList1.Rulelist.Add(new USBasedRequestRule() { Resource = "bbb", TargetResult =true }); + ThirdLevelRuleList1.Rulelist.Add(new MaxPerTimeSpanRule() { Resource = "bbb", maxcount=5, timespan=2 }); + + ThirdLevelRuleList2.Rulelist.Add(new USBasedRequestRule() { Resource = "bbb", TargetResult = false }); + ThirdLevelRuleList2.Rulelist.Add(new MinTimeSpanLapsRule() { Resource = "bbb", Laps=2 }); + + //eng.StartingRuleList.Rulelist.Add(secondLevelRuleList); + eng.StartingRuleList.Rulelist.Add(secondLevelRuleList); + + + } + + + [TestMethod] + public async Task Rule1Met () + { + var dt = DateTime.Now; + bool res = true; + for (int i=0; i<3; i++) + { + dt = dt.AddMilliseconds(10); + var Request = new ResourceRequest { DateTime = dt, Resource = "aaa" }; + res = res && await eng.Evaluate(Request, "US32fdgfdgbfdklfds"); + } + Assert.IsTrue(res); + } + [TestMethod] + public async Task Rule1NotMet() + { + var dt = DateTime.Now; + bool res = true; + for (int i = 0; i < 10; i++) + { + dt = dt.AddMilliseconds(10); + var Request = new ResourceRequest { DateTime = dt, Resource = "aaa" }; + res = res && await eng.Evaluate(Request, "US32fdgfdgbfdklfds"); + } + Assert.IsFalse(res); + } + + [TestMethod] + public async Task Rule2NotMet() + { + var dt = DateTime.Now; + string resourceString = "aaa"; + bool res = true; + for (int i = 0; i < 11; i++) + { + dt = dt.AddMilliseconds(1); + resourceString = resourceString + "a"; + var Request = new ResourceRequest { DateTime = dt, Resource = resourceString }; + res = res && await eng.Evaluate(Request, "US32fdgfdgbfdklfds"); + } + Assert.IsFalse(res); + } + + + + [TestMethod] + public async Task TestHasRules() + { + var Request = new ResourceRequest { DateTime = DateTime.Now, Resource = "aaa" }; + var client = new ReqClient("US1cliwndjaslh") { Region = "EU", Subscription = "1" }; + Assert.IsTrue(await eng.Evaluate(Request,client)); + + + } + [TestMethod] + public async Task Rule5And6Met() + { + var Request = new ResourceRequest { DateTime = DateTime.Now, Resource = "bbb" }; + await eng.Evaluate(Request, "EU2gbfdklfds"); + var NewRequest = new ResourceRequest { DateTime = Request.DateTime.AddHours(1), Resource = "bbb" }; + var res = await eng.Evaluate(NewRequest, "EU2gbfdklfds"); + Assert.IsTrue(res); + } + [TestMethod] + public async Task Rule5MetAnd6NotMet() + { + var Request = new ResourceRequest { DateTime = DateTime.Now, Resource = "bbb" }; + await eng.Evaluate(Request, "EU2gbfdklfds"); + var NewRequest = new ResourceRequest { DateTime = Request.DateTime.AddSeconds(1), Resource = "bbb" }; + var res = await eng.Evaluate(NewRequest, "EU2gbfdklfds"); + Assert.IsFalse(res); + } + + [TestMethod] + public async Task Rule3And4Met() + { + var dt = DateTime.Now; + bool res = true; + for (int i = 0; i < 3; i++) + { + dt= dt.AddMilliseconds(100); + var Request = new ResourceRequest { DateTime = dt, Resource = "bbb" }; + res= res && await eng.Evaluate(Request, "US2gbfdklfds"); + } + + Assert.IsTrue(res); + } + [TestMethod] + public async Task Rule3And4NotMet() + { + var dt = DateTime.Now; + bool res = true; + for (int i = 0; i < 10; i++) + { + dt = dt.AddMilliseconds(100); + var Request = new ResourceRequest { DateTime = dt, Resource = "bbb" }; + res = res && await eng.Evaluate(Request, "US2gbfdklfds"); + } + + Assert.IsFalse(res); + } + + + + } +} \ No newline at end of file diff --git a/RateLimiter.sln b/RateLimiter.sln index 626a7bfa..67126f4e 100644 --- a/RateLimiter.sln +++ b/RateLimiter.sln @@ -1,17 +1,17 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.15 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B206889-9841-4B5E-B79B-D5B2610CCCFF}" ProjectSection(SolutionItems) = preProject README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSTestRateLimiter.Test", "MSTestRateLimiter.Test\MSTestRateLimiter.Test.csproj", "{74BAFF67-2F4C-443C-B1D0-4602F6ACAE61}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -22,10 +22,10 @@ Global {36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Debug|Any CPU.Build.0 = Debug|Any CPU {36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Release|Any CPU.ActiveCfg = Release|Any CPU {36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Release|Any CPU.Build.0 = Release|Any CPU - {C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Release|Any CPU.Build.0 = Release|Any CPU + {74BAFF67-2F4C-443C-B1D0-4602F6ACAE61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74BAFF67-2F4C-443C-B1D0-4602F6ACAE61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74BAFF67-2F4C-443C-B1D0-4602F6ACAE61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74BAFF67-2F4C-443C-B1D0-4602F6ACAE61}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/RateLimiter/Interface/IClient.cs b/RateLimiter/Interface/IClient.cs new file mode 100644 index 00000000..28b9a8d9 --- /dev/null +++ b/RateLimiter/Interface/IClient.cs @@ -0,0 +1,14 @@ + +using System.Collections.Generic; + +namespace RateLimiter.Interface +{ + public interface IClient + { + string Token { get; } + string Region { get; set; } + string Subscription {get;set;} + + List resourceRequests { get; set; } + } +} diff --git a/RateLimiter/Interface/IRequestService.cs b/RateLimiter/Interface/IRequestService.cs new file mode 100644 index 00000000..55e94068 --- /dev/null +++ b/RateLimiter/Interface/IRequestService.cs @@ -0,0 +1,9 @@ +using RateLimiter.Interface; + +namespace RateLimiter.Interface +{ + public interface IRequestService + { + IClient GetClient(string token); + } +} \ No newline at end of file diff --git a/RateLimiter/Interface/IResourceRequest.cs b/RateLimiter/Interface/IResourceRequest.cs new file mode 100644 index 00000000..9ee3896e --- /dev/null +++ b/RateLimiter/Interface/IResourceRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace RateLimiter.Interface +{ + public interface IResourceRequest + { + DateTime DateTime { get; set; } + string Resource { get; set; } + } +} \ No newline at end of file diff --git a/RateLimiter/Interface/IRuleBase.cs b/RateLimiter/Interface/IRuleBase.cs new file mode 100644 index 00000000..1e3904de --- /dev/null +++ b/RateLimiter/Interface/IRuleBase.cs @@ -0,0 +1,12 @@ + + +using System.Threading.Tasks; + +namespace RateLimiter.Interface +{ + public interface IRuleBase + { + string Resource { get; set; } + Task Evaluate(IClient client, IResourceRequest currentRequest); + } +} diff --git a/RateLimiter/Model/ReqClient.cs b/RateLimiter/Model/ReqClient.cs new file mode 100644 index 00000000..8b9e8697 --- /dev/null +++ b/RateLimiter/Model/ReqClient.cs @@ -0,0 +1,15 @@ +using RateLimiter.Interface; +using System.Collections.Generic; + +namespace RateLimiter.Model +{ + public class ReqClient : IClient + { + public ReqClient(string token) { Token = token; } + public string Token { get; } + + public string Region { get; set ; } + public string Subscription { get; set ; } + public List resourceRequests { get ; set ; } = new List (); + } +} diff --git a/RateLimiter/Model/ResourceRequest.cs b/RateLimiter/Model/ResourceRequest.cs new file mode 100644 index 00000000..0c4cb91c --- /dev/null +++ b/RateLimiter/Model/ResourceRequest.cs @@ -0,0 +1,12 @@ +using RateLimiter.Interface; +using System; + + +namespace RateLimiter.Model +{ + public class ResourceRequest : IResourceRequest + { + public string Resource { get; set; } + public DateTime DateTime { get; set; } + } +} diff --git a/RateLimiter/Model/Rules/MaxPerTimeSpanRule.cs b/RateLimiter/Model/Rules/MaxPerTimeSpanRule.cs new file mode 100644 index 00000000..f72eb455 --- /dev/null +++ b/RateLimiter/Model/Rules/MaxPerTimeSpanRule.cs @@ -0,0 +1,31 @@ +using RateLimiter.Interface; +using System.Linq; +using System.Threading.Tasks; + +namespace RateLimiter.Model.Rules +{ + public class MaxPerTimeSpanRule : IRuleBase + { + + + public string? Resource { get; set; } + public int maxcount { get; set; } + public int timespan { get; set; } + + public async Task Evaluate(IClient client, IResourceRequest currentRequest) + { + var thisRequestTime = currentRequest.DateTime; + var filterTime = thisRequestTime.AddSeconds(-timespan); + int previousRquestsCount; + if (Resource == null) // rule is max number of request whichever resource + previousRquestsCount = client.resourceRequests.Where(r => r.DateTime >= filterTime).Count(); + else if (Resource == currentRequest.Resource) + previousRquestsCount = client.resourceRequests.Where(r => r.DateTime >= filterTime && r.Resource == Resource).Count(); + else // rule does not apply to this request, so pass + return true; + return previousRquestsCount < maxcount; + } + + + } +} diff --git a/RateLimiter/Model/Rules/MaxTimeSpanLapsRule.cs b/RateLimiter/Model/Rules/MaxTimeSpanLapsRule.cs new file mode 100644 index 00000000..8f33e8ca --- /dev/null +++ b/RateLimiter/Model/Rules/MaxTimeSpanLapsRule.cs @@ -0,0 +1,33 @@ + +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Threading.Tasks; +using RateLimiter.Interface; +using RateLimiter.Model; + +namespace RateLimiter.Model.Rules +{ + public class MaxTimeSpanLapsRule : IRuleBase + { + public string? Resource { get; set; } + public int Laps { get; set; } //assume seconds but could be configurable + + public async Task Evaluate(IClient client, IResourceRequest currentRequest) + { + ResourceRequest? lastResourceRequest = null; + if (Resource == null) // rule is max number of request whichever resource + lastResourceRequest = (ResourceRequest)client.resourceRequests?.LastOrDefault(); + + else if (Resource == currentRequest.Resource) + lastResourceRequest = (ResourceRequest)client.resourceRequests?.LastOrDefault(r => r.Resource == Resource); + + if (lastResourceRequest != null) + { + var diffOfDates = currentRequest.DateTime - lastResourceRequest.DateTime; + return diffOfDates.Seconds < Laps; + } + return true; + + } + } +} diff --git a/RateLimiter/Model/Rules/MinTimeSpanLapsRule.cs b/RateLimiter/Model/Rules/MinTimeSpanLapsRule.cs new file mode 100644 index 00000000..ac868de6 --- /dev/null +++ b/RateLimiter/Model/Rules/MinTimeSpanLapsRule.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Threading.Tasks; +using RateLimiter.Interface; +using RateLimiter.Model; + +namespace RateLimiter.Model.Rules +{ + public class MinTimeSpanLapsRule : IRuleBase + { + public string? Resource { get; set; } + public int Laps { get; set; } //assume seconds but could be configurable + + + public async Task Evaluate(IClient client, IResourceRequest currentRequest) + { + ResourceRequest? lastResourceRequest = null; + if (Resource == null) // rule is max number of request whichever resource + lastResourceRequest = (ResourceRequest)client.resourceRequests?.LastOrDefault(); + + else if (Resource == currentRequest.Resource) + lastResourceRequest = (ResourceRequest)client.resourceRequests?.LastOrDefault(r => r.Resource == Resource); + + if (lastResourceRequest != null) + { + var diffOfDates = currentRequest.DateTime - lastResourceRequest.DateTime; + return diffOfDates.TotalSeconds > Laps; + } + return true; + + } + } +} diff --git a/RateLimiter/Model/Rules/RuleList.cs b/RateLimiter/Model/Rules/RuleList.cs new file mode 100644 index 00000000..982f8f71 --- /dev/null +++ b/RateLimiter/Model/Rules/RuleList.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interface; +using RateLimiter.Model; + +namespace RateLimiter.Model.Rules +{ + public class RuleList : IRuleBase + { + public string Resource { get; set; } + public List Rulelist { get; } = new List(); + public List RuleResults { get; } = new List(); + public string ListOperand { get; set; } = "And"; + + public async Task Evaluate(IClient client, IResourceRequest req) + { + RuleResults.Clear(); + foreach (IRuleBase rule in Rulelist) + { + RuleResults.Add(await rule.Evaluate(client, req)); + + } + + return await GetEvalResults(); + } + + private async Task GetEvalResults() + { + bool result; + switch (ListOperand.ToLower()) + { + case "or": + case "||": + result = false; + foreach (var item in RuleResults) + { + result = result || item; + } + break; + case "and": + case "&&": + default: + result = true; + foreach (var item in RuleResults) + { + result = result && item; + } + break; + + } + return result; + } + } +} diff --git a/RateLimiter/Model/Rules/USBasedRequestRule.cs b/RateLimiter/Model/Rules/USBasedRequestRule.cs new file mode 100644 index 00000000..ee75d6a7 --- /dev/null +++ b/RateLimiter/Model/Rules/USBasedRequestRule.cs @@ -0,0 +1,21 @@ +using RateLimiter.Interface; +using System.Threading.Tasks; + +namespace RateLimiter.Model.Rules +{ + public class USBasedRequestRule : IRuleBase + { + public string Resource { get; set; } + + public bool TargetResult { get; set; } = true; + public async Task Evaluate(IClient client, IResourceRequest currentRequest) + { + bool resourceMatched = (Resource == null) || (Resource == currentRequest.Resource); + if (!resourceMatched) + return true; + + var isUSBased = client.Region == "US"; + return (isUSBased == TargetResult); + } + } +} diff --git a/RateLimiter/Service/RateLimitingEngine.cs b/RateLimiter/Service/RateLimitingEngine.cs new file mode 100644 index 00000000..d531e308 --- /dev/null +++ b/RateLimiter/Service/RateLimitingEngine.cs @@ -0,0 +1,35 @@ +using RateLimiter.Interface; +using RateLimiter.Model.Rules; +using RateLimiter.Interface; +using RateLimiter.Model; +using System.Threading.Tasks; +using System; + +namespace RateLimiter.Service +{ + public class RateLimitingEngine + { + IRequestService _reqsvc; + public RateLimitingEngine(IRequestService reqsvc) + { + _reqsvc = reqsvc; + } + public RuleList StartingRuleList { get; set; }= new RuleList(); + public async Task Evaluate(ResourceRequest req, IClient client) + { + var res = await StartingRuleList.Evaluate(client, req); + client.resourceRequests.Add(req); + return res; + + } + public async Task Evaluate(ResourceRequest req, String token) + { + var client = _reqsvc.GetClient(token); + var res = await StartingRuleList.Evaluate(client, req); + client.resourceRequests.Add(req); + return res; + + } + + } +} diff --git a/RateLimiter/Service/RequestService.cs b/RateLimiter/Service/RequestService.cs new file mode 100644 index 00000000..7c028270 --- /dev/null +++ b/RateLimiter/Service/RequestService.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interface; +using RateLimiter.Interface; +using RateLimiter.Model; + +namespace RateLimiter.Service +{ + public class RequestService : IRequestService + { + private List reqClients = new List(); + public IClient GetClient(string token) + { + // In a production environment this method would find and lookup the client record from the token by whatever + // lookup means was designed + // the reqClients list might be a Redis table used as a cache and fast data retrieval mechanism + + if (reqClients.Exists(r => r.Token == token)) + return reqClients.FirstOrDefault(r => r.Token == token); + + var client = new ReqClient(token); + // obviously convention for demo purposes + if (token.ToUpper().StartsWith("US")) + client.Region = "US"; + else + client.Region = "EU"; + client.Subscription = token[3].ToString(); + reqClients.Add(client); + return client; + } + } +}