├── samples ├── aspnet │ ├── Global.asax │ ├── Models │ │ └── WeatherForecast.cs │ ├── App_Start │ │ └── WebApiConfig.cs │ ├── Controllers │ │ └── WeatherForecastController.cs │ ├── Global.asax.cs │ ├── packages.config │ └── Web.config ├── IPOrClientId │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── WeatherForecast.cs │ ├── IPOrClientId.csproj │ ├── Properties │ │ └── launchSettings.json │ ├── Controllers │ │ └── WeatherForecastController.cs │ └── Program.cs ├── RuleAutoUpdate │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── WeatherForecast.cs │ ├── RuleAutoUpdate.csproj │ ├── RateLimit │ │ ├── AutoUpdateAlgorithmService.cs │ │ ├── NonCapturingTimer.cs │ │ ├── RateLimitRuleDAO.cs │ │ ├── RateLimitConfigurationManager.cs │ │ └── AutoUpdateAlgorithmManager.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Program.cs │ └── Controllers │ │ └── WeatherForecastController.cs ├── aspnetcore │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── WeatherForecast.cs │ ├── AspNetCore.csproj │ ├── Properties │ │ └── launchSettings.json │ ├── Program.cs │ └── Controllers │ │ └── WeatherForecastController.cs └── console │ └── Console.csproj ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── FireflySoft.RateLimit.AspNetCore ├── HttpRateLimitError.cs ├── Properties │ └── launchSettings.json ├── FireflySoft.RateLimit.AspNetCore.csproj ├── HttpErrorResponse.cs ├── README.md ├── HttpInvokeInterceptor.cs └── RateLimitService.cs ├── FireflySoft.RateLimit.Core.Test ├── SimulationRequest.cs ├── RedisClientHelper.cs ├── FireflySoft.RateLimit.Core.Test.csproj ├── CounterDictionaryTest.cs ├── TestTimeProvider.cs ├── LocalTimeProviderTest.cs ├── RedisTimeProviderTest.cs ├── AlgorithmStartTimeTest.cs ├── AlgorithmCheckResultTest.cs └── MemorySlidingWindowTest.cs ├── FireflySoft.RateLmit.Core.BenchmarkTest ├── Program.cs └── FireflySoft.RateLmit.Core.BenchmarkTest.csproj ├── FireflySoft.RateLimit.Core ├── InProcessAlgorithm │ ├── TokenBucketCounter.cs │ ├── LeakyBucketCounter.cs │ ├── FixedWindowCounter.cs │ ├── BaseInProcessAlgorithm.cs │ ├── InProcessSlidingWindowAlgorithm.cs │ ├── CounterDictionary.cs │ ├── InProcessTokenBucketAlgorithm.cs │ ├── MemorySlidingWindow.cs │ └── InProcessLeakyBucketAlgorithm.cs ├── Rule │ ├── StartTimeType.cs │ ├── FixedWindowRule.cs │ ├── TokenBucketRule.cs │ ├── RateLimitRule.cs │ ├── SlidingWindowRule.cs │ └── LeakyBucketRule.cs ├── AlgorithmCheckResult.cs ├── Time │ ├── ITimeProvider.cs │ ├── LocalTimeProvider.cs │ ├── RedisTimeProvider.cs │ └── AlgorithmStartTime.cs ├── IAlgorithm.cs ├── RuleCheckResult.cs ├── FireflySoft.RateLimit.Core.csproj ├── README.md └── RedisAlgorithm │ ├── RedisTokenBucketAlgorithm.cs │ └── RedisSlidingWindowAlgorithm.cs ├── FireflySoft.RateLimit.AspNet ├── packages.config ├── Properties │ └── AssemblyInfo.cs ├── HttpRateLimitError.cs ├── RateLimitHandler.cs └── FireflySoft.RateLimit.AspNet.csproj ├── README.zh-CN.md ├── FireflySoft.RateLimit.sln ├── README.md └── .gitignore /samples/aspnet/Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Inherits="FireflySoft.RateLimit.AspNet.Sample.Global" %> 2 | -------------------------------------------------------------------------------- /samples/IPOrClientId/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /samples/IPOrClientId/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /samples/aspnetcore/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/aspnetcore/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet-test-explorer.testProjectPath": "**/*Test.@(csproj|vbproj|fsproj)", 3 | "dotnet-test-explorer.testArguments": "/p:CollectCoverage=true /p:CoverletOutput=./TestResults/ /p:CoverletOutputFormat=opencover", 4 | "licenser.author": "bosima" 5 | } -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "DbConn":"Server=127.0.0.1;User ID=root;Password=l123456;port=3306;Database=ratelimit;CharSet=utf8mb4;", 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /samples/IPOrClientId/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace IPOrClientId; 2 | 3 | public class WeatherForecast 4 | { 5 | public DateTime Date { get; set; } 6 | 7 | public int TemperatureC { get; set; } 8 | 9 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 10 | 11 | public string? Summary { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace RuleAutoUpdate; 2 | 3 | public class WeatherForecast 4 | { 5 | public DateTime Date { get; set; } 6 | 7 | public int TemperatureC { get; set; } 8 | 9 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 10 | 11 | public string? Summary { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNetCore/HttpRateLimitError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireflySoft.RateLimit.AspNetCore 4 | { 5 | /// 6 | /// Http Rate Limit Error 7 | /// 8 | [Obsolete("The class name is not clear, please use HttpErrorResponse")] 9 | public class HttpRateLimitError : HttpErrorResponse 10 | { 11 | 12 | } 13 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/SimulationRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace FireflySoft.RateLimit.Core.Test 4 | { 5 | public class SimulationRequest 6 | { 7 | public string RequestId { get; set; } 8 | 9 | public string RequestResource { get; set; } 10 | 11 | public Dictionary Parameters { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /samples/console/Console.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /samples/aspnetcore/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireflySoft.RateLimit.AspNetCore.Sample 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/RedisClientHelper.cs: -------------------------------------------------------------------------------- 1 | public class RedisClientHelper{ 2 | private static StackExchange.Redis.ConnectionMultiplexer _redisClient; 3 | public static StackExchange.Redis.ConnectionMultiplexer GetClient() 4 | { 5 | _redisClient = StackExchange.Redis.ConnectionMultiplexer.Connect("127.0.0.1"); 6 | _redisClient.GetDatabase(0).StringGet("TestConnect"); 7 | return _redisClient; 8 | } 9 | } -------------------------------------------------------------------------------- /samples/aspnet/Models/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireflySoft.RateLimit.AspNet.Sample.Models 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/aspnetcore/AspNetCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /FireflySoft.RateLmit.Core.BenchmarkTest/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Attributes; 3 | using BenchmarkDotNet.Running; 4 | using BenchmarkDotNet.Validators; 5 | 6 | namespace FireflySoft.RateLmit.Core.BenchmarkTest 7 | { 8 | class Program 9 | { 10 | static void Main(string[] args) 11 | { 12 | var benchmark = BenchmarkRunner.Run(); 13 | Console.Read(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNetCore/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true 5 | }, 6 | "profiles": { 7 | "FireflySoft.RateLimit.AspNetCore": { 8 | "commandName": "Project", 9 | "launchBrowser": true, 10 | "applicationUrl": "http://localhost:20120", 11 | "environmentVariables": { 12 | "ASPNETCORE_ENVIRONMENT": "Development" 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /FireflySoft.RateLmit.Core.BenchmarkTest/FireflySoft.RateLmit.Core.BenchmarkTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1;net6; 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/IPOrClientId/IPOrClientId.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/aspnetcore/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:44035", 8 | "sslPort": 44352 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast/today", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /samples/aspnet/App_Start/WebApiConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Web.Http; 2 | 3 | namespace FireflySoft.RateLimit.AspNet.Sample 4 | { 5 | public static class WebApiConfig 6 | { 7 | public static void Register(HttpConfiguration config) 8 | { 9 | // Web API configuration and services 10 | 11 | // Web API routes 12 | config.MapHttpAttributeRoutes(); 13 | 14 | config.Routes.MapHttpRoute( 15 | name: "DefaultApi", 16 | routeTemplate: "{controller}/{id}", 17 | defaults: new { id = RouteParameter.Optional } 18 | ); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/InProcessAlgorithm/TokenBucketCounter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireflySoft.RateLimit.Core.InProcessAlgorithm 4 | { 5 | /// 6 | /// Define a counter for token bucket algorithm 7 | /// 8 | public class TokenBucketCounter 9 | { 10 | /// 11 | /// The Count Value 12 | /// 13 | /// 14 | public long Value { get; set; } 15 | 16 | /// 17 | /// The last inflow time 18 | /// 19 | /// 20 | public DateTimeOffset LastInflowTime { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNet/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Rule/StartTimeType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace FireflySoft.RateLimit.Core 3 | { 4 | /// 5 | /// The type of statistics start time 6 | /// 7 | public enum StartTimeType 8 | { 9 | /// 10 | /// From the current time. 11 | /// 12 | FromCurrent = 1, 13 | 14 | /// 15 | /// From the beginning of the natural period. 16 | /// In this way, the statistical time window must be an integer and coincide with the natural time period. 17 | /// This type is not valid for statistical time windows less than 1 second, which is equivalent to 'FromCurrent' type. 18 | /// 19 | FromNaturalPeriodBeign = 2 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/RuleAutoUpdate.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/aspnetcore/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace FireflySoft.RateLimit.AspNetCore.Sample 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/RateLimit/AutoUpdateAlgorithmService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RuleAutoUpdate.RateLimit 4 | { 5 | public static class AutoUpdateAlgorithmService 6 | { 7 | /// 8 | /// Add auto update rate limit algorithm service 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | public static IServiceCollection AddAutoUpdateRateLimitAlgorithm(this IServiceCollection services) 16 | { 17 | services.AddSingleton(); 18 | services.AddSingleton(); 19 | services.AddSingleton(); 20 | return services; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/IPOrClientId/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:17675", 8 | "sslPort": 44356 9 | } 10 | }, 11 | "profiles": { 12 | "IPOrClientId": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7137;http://localhost:5217", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:5777", 8 | "sslPort": 44342 9 | } 10 | }, 11 | "profiles": { 12 | "aspnetcore6": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7265;http://localhost:5092", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/FireflySoft.RateLimit.Core.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | runtime; build; native; contentfiles; analyzers; buildtransitive 11 | all 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/InProcessAlgorithm/LeakyBucketCounter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading.Tasks; 5 | using FireflySoft.RateLimit.Core.Rule; 6 | using FireflySoft.RateLimit.Core.Time; 7 | 8 | namespace FireflySoft.RateLimit.Core.InProcessAlgorithm 9 | { 10 | /// 11 | /// Define a counter for leaky bucket algorithm 12 | /// 13 | public class LeakyBucketCounter 14 | { 15 | /// 16 | /// The number of requests that allowed to be processed in the current time window, 17 | /// including the requests in the leaky bucket and the requests that have flowed out in the current time window. 18 | /// 19 | /// 20 | public long Value { get; set; } 21 | 22 | /// 23 | /// The last flow-out time 24 | /// 25 | /// 26 | public DateTimeOffset LastFlowOutTime { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/InProcessAlgorithm/FixedWindowCounter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading.Tasks; 5 | using FireflySoft.RateLimit.Core.Rule; 6 | using FireflySoft.RateLimit.Core.Time; 7 | 8 | namespace FireflySoft.RateLimit.Core.InProcessAlgorithm 9 | { 10 | /// 11 | /// Define a counter for fixed window algorithm 12 | /// 13 | public class FixedWindowCounter 14 | { 15 | /// 16 | /// The Count Value 17 | /// 18 | /// 19 | public long Value { get; set; } 20 | 21 | /// 22 | /// The start time of current window 23 | /// 24 | /// 25 | public DateTimeOffset StartTime { get; set; } 26 | 27 | /// 28 | /// The statistical time window 29 | /// 30 | /// 31 | public TimeSpan StatWindow { get; set; } 32 | } 33 | } -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/RateLimit/NonCapturingTimer.cs: -------------------------------------------------------------------------------- 1 | namespace RuleAutoUpdate.RateLimit; 2 | 3 | internal static class NonCapturingTimer 4 | { 5 | public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) 6 | { 7 | if (callback == null) 8 | { 9 | throw new ArgumentNullException(nameof(callback)); 10 | } 11 | 12 | // Don't capture the current ExecutionContext and its AsyncLocals onto the timer 13 | bool restoreFlow = false; 14 | try 15 | { 16 | if (!ExecutionContext.IsFlowSuppressed()) 17 | { 18 | ExecutionContext.SuppressFlow(); 19 | restoreFlow = true; 20 | } 21 | 22 | return new Timer(callback, state, dueTime, period); 23 | } 24 | finally 25 | { 26 | // Restore the current ExecutionContext 27 | if (restoreFlow) 28 | { 29 | ExecutionContext.RestoreFlow(); 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Rule/FixedWindowRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FireflySoft.RateLimit.Core.Rule 5 | { 6 | /// 7 | /// Fixed Window Algorithm 8 | /// 9 | public class FixedWindowRule : RateLimitRule 10 | { 11 | /// 12 | /// The statistical time window, which counts the number of requests in this time. 13 | /// When using redis storage, it needs to be an integral multiple of one second. 14 | /// 15 | public TimeSpan StatWindow { get; set; } 16 | 17 | /// 18 | /// The threshold of triggering rate limiting in the statistical time window. 19 | /// If less than 0, it means no limit. 20 | /// 21 | public int LimitNumber { get; set; } 22 | 23 | /// 24 | /// Get the rate limit threshold. 25 | /// 26 | /// 27 | public override long GetLimitThreshold() 28 | { 29 | return LimitNumber; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /samples/aspnet/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web.Http; 5 | using FireflySoft.RateLimit.AspNet.Sample.Models; 6 | 7 | namespace FireflySoft.RateLimit.AspNet.Sample.Controllers 8 | { 9 | public class WeatherForecastController : ApiController 10 | { 11 | private static readonly string[] Summaries = new[] 12 | { 13 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 14 | }; 15 | 16 | public WeatherForecastController() 17 | { 18 | } 19 | 20 | [HttpGet] 21 | public IEnumerable Get() 22 | { 23 | var rng = new Random(); 24 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 25 | { 26 | Date = DateTime.Now.AddDays(index), 27 | TemperatureC = rng.Next(-20, 55), 28 | Summary = Summaries[rng.Next(Summaries.Length)] 29 | }) 30 | .ToArray(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNet/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | 4 | // Information about this assembly is defined by the following attributes. 5 | // Change them to the values specific to your project. 6 | 7 | [assembly: AssemblyTitle("FireflySoft.RateLimit.AspNet")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("fireflysoft.net")] 11 | [assembly: AssemblyProduct("")] 12 | [assembly: AssemblyCopyright("${AuthorCopyright}")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". 17 | // The form "{Major}.{Minor}.*" will automatically update the build and revision, 18 | // and "{Major}.{Minor}.{Build}.*" will update just the revision. 19 | 20 | [assembly: AssemblyVersion("1.2.0")] 21 | [assembly: AssemblyFileVersion("1.2.0")] 22 | 23 | // The following attributes are used to specify the signing key for the assembly, 24 | // if desired. See the Mono documentation for more information about signing. 25 | 26 | //[assembly: AssemblyDelaySign(false)] 27 | //[assembly: AssemblyKeyFile("")] 28 | -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/Program.cs: -------------------------------------------------------------------------------- 1 | using RuleAutoUpdate.RateLimit; 2 | using FireflySoft.RateLimit.AspNetCore; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | // Add services to the container. 7 | 8 | builder.Services.AddControllers(); 9 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 10 | builder.Services.AddEndpointsApiExplorer(); 11 | builder.Services.AddSwaggerGen(); 12 | 13 | // Add firefly soft rate limit service 14 | builder.Services.AddAutoUpdateRateLimitAlgorithm(); 15 | builder.Services.AddRateLimit(serviceProvider => 16 | { 17 | var algorithmManager = serviceProvider.GetService(); 18 | if (algorithmManager != null) 19 | { 20 | return algorithmManager.GetAlgorithmInstance(); 21 | } 22 | 23 | return null; 24 | }); 25 | 26 | var app = builder.Build(); 27 | 28 | // Configure the HTTP request pipeline. 29 | if (app.Environment.IsDevelopment()) 30 | { 31 | app.UseSwagger(); 32 | app.UseSwaggerUI(); 33 | } 34 | 35 | app.UseHttpsRedirection(); 36 | 37 | app.UseAuthorization(); 38 | 39 | // Use firefly soft rate limit middleware 40 | app.UseRateLimit(); 41 | 42 | app.MapControllers(); 43 | 44 | app.Run(); 45 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/AlgorithmCheckResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | 6 | namespace FireflySoft.RateLimit.Core 7 | { 8 | /// 9 | /// Algorithm Check Result 10 | /// 11 | public class AlgorithmCheckResult 12 | { 13 | private IEnumerable _ruleCheckResults; 14 | 15 | /// 16 | /// Create a new instance. 17 | /// 18 | /// 19 | public AlgorithmCheckResult(IEnumerable ruleCheckResults) 20 | { 21 | _ruleCheckResults = ruleCheckResults; 22 | } 23 | 24 | /// 25 | /// If true, it means that the current request should be limited 26 | /// 27 | /// 28 | public bool IsLimit 29 | { 30 | get 31 | { 32 | return _ruleCheckResults.Any(d => d.IsLimit); 33 | } 34 | } 35 | 36 | /// 37 | /// The rule check results. 38 | /// 39 | /// 40 | public IEnumerable RuleCheckResults 41 | { 42 | get 43 | { 44 | return _ruleCheckResults; 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /samples/aspnet/Global.asax.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Web; 4 | using System.Web.Http; 5 | using FireflySoft.RateLimit.Core; 6 | using FireflySoft.RateLimit.Core.Rule; 7 | 8 | namespace FireflySoft.RateLimit.AspNet.Sample 9 | { 10 | public class Global : HttpApplication 11 | { 12 | protected void Application_Start() 13 | { 14 | GlobalConfiguration.Configuration.MessageHandlers.Add(new RateLimitHandler( 15 | new Core.InProcessAlgorithm.InProcessFixedWindowAlgorithm( 16 | new[] { 17 | new FixedWindowRule() 18 | { 19 | ExtractTarget = context => 20 | { 21 | return (context as HttpRequestMessage).RequestUri.AbsolutePath; 22 | }, 23 | CheckRuleMatching = context => 24 | { 25 | return true; 26 | }, 27 | Name="default limit rule", 28 | LimitNumber=30, 29 | StatWindow=TimeSpan.FromSeconds(1) 30 | } 31 | }) 32 | )); 33 | 34 | GlobalConfiguration.Configure(WebApiConfig.Register); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /samples/aspnetcore/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace FireflySoft.RateLimit.AspNetCore.Sample.Controllers 9 | { 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class WeatherForecastController : ControllerBase 13 | { 14 | private static readonly string[] Summaries = new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }; 18 | 19 | private readonly ILogger _logger; 20 | 21 | public WeatherForecastController(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | [HttpGet] 27 | public IEnumerable Get() 28 | { 29 | var rng = new Random(); 30 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 31 | { 32 | Date = DateTime.Now.AddDays(index), 33 | TemperatureC = rng.Next(-20, 55), 34 | Summary = Summaries[rng.Next(Summaries.Length)] 35 | }) 36 | .ToArray(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Time/ITimeProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace FireflySoft.RateLimit.Core.Time 5 | { 6 | /// 7 | /// The interface for time provider 8 | /// 9 | public interface ITimeProvider 10 | { 11 | /// 12 | /// Get the milliseconds of current unix time 13 | /// 14 | /// 15 | long GetCurrentUtcMilliseconds(); 16 | 17 | /// 18 | /// Get current utc time 19 | /// 20 | /// 21 | DateTimeOffset GetCurrentUtcTime(); 22 | 23 | /// 24 | /// Get current local time 25 | /// 26 | /// 27 | DateTimeOffset GetCurrentLocalTime(); 28 | 29 | /// 30 | /// Get the milliseconds of current unix time 31 | /// 32 | /// 33 | Task GetCurrentUtcMillisecondsAsync(); 34 | 35 | /// 36 | /// Get current local time 37 | /// 38 | /// 39 | Task GetCurrentLocalTimeAsync(); 40 | 41 | /// 42 | /// Get current utc time 43 | /// 44 | /// 45 | Task GetCurrentUtcTimeAsync(); 46 | } 47 | } -------------------------------------------------------------------------------- /samples/IPOrClientId/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace IPOrClientId.Controllers; 4 | 5 | [ApiController] 6 | [Route("[controller]")] 7 | public class WeatherForecastController : ControllerBase 8 | { 9 | private static readonly string[] Summaries = new[] 10 | { 11 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 12 | }; 13 | 14 | private readonly ILogger _logger; 15 | 16 | public WeatherForecastController(ILogger logger) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | [HttpGet("Future")] 22 | public IEnumerable GetFuture() 23 | { 24 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 25 | { 26 | Date = DateTime.Now.AddDays(index), 27 | TemperatureC = Random.Shared.Next(-20, 55), 28 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 29 | }) 30 | .ToArray(); 31 | } 32 | 33 | [HttpGet("Today")] 34 | public WeatherForecast GetToday() 35 | { 36 | return new WeatherForecast 37 | { 38 | Date = DateTime.Now, 39 | TemperatureC = Random.Shared.Next(-20, 55), 40 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace RuleAutoUpdate.Controllers; 4 | 5 | [ApiController] 6 | [Route("[controller]")] 7 | public class WeatherForecastController : ControllerBase 8 | { 9 | private static readonly string[] Summaries = new[] 10 | { 11 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 12 | }; 13 | 14 | private readonly ILogger _logger; 15 | 16 | public WeatherForecastController(ILogger logger) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | [HttpGet("GetToday")] 22 | public IEnumerable GetToday(string userId) 23 | { 24 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 25 | { 26 | Date = DateTime.Now.AddDays(index), 27 | TemperatureC = Random.Shared.Next(-20, 55), 28 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 29 | }) 30 | .ToArray(); 31 | } 32 | 33 | [HttpGet("GetTomorrow")] 34 | public IEnumerable GetTomorrow(string userId) 35 | { 36 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 37 | { 38 | Date = DateTime.Now.AddDays(index), 39 | TemperatureC = Random.Shared.Next(-20, 55), 40 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 41 | }) 42 | .ToArray(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/IAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using FireflySoft.RateLimit.Core.Rule; 4 | 5 | namespace FireflySoft.RateLimit.Core 6 | { 7 | /// 8 | /// Defines a mechanism for using rate limit algorithm. 9 | /// 10 | public interface IAlgorithm 11 | { 12 | /// 13 | /// Check the request and return the rate limit result 14 | /// 15 | /// 16 | /// 17 | AlgorithmCheckResult Check(object request); 18 | 19 | /// 20 | /// Check the request and return the rate limit result 21 | /// 22 | /// 23 | /// 24 | Task CheckAsync(object request); 25 | 26 | /// 27 | /// Update the rate limit rules 28 | /// 29 | /// 30 | void UpdateRules(IEnumerable rules); 31 | 32 | /// 33 | /// Update the rate limit rules 34 | /// 35 | /// 36 | Task UpdateRulesAsync(IEnumerable rules); 37 | 38 | /// 39 | /// Peek at the rate limit check results at the current time. 40 | /// 41 | /// 42 | /// 43 | AlgorithmCheckResult Peek(string target); 44 | 45 | /// 46 | /// Peek at the rate limit check results at the current time. 47 | /// 48 | /// 49 | /// 50 | Task PeekAsync(string target); 51 | } 52 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNetCore/FireflySoft.RateLimit.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1;net5;net6 5 | Library 6 | FireflySoft.RateLimit.AspNetCore 7 | 3.0.0 8 | bossma 9 | https://github.com/bosima/FireflySoft.RateLimit 10 | 11 | Return X-RateLimit-XXX in HTTP response. 12 | 13 | ASP.NET Core;Rate Limit;Fixed Window;Sliding Window;Leaky Bucket;Token Bucket 14 | 15 | A rate limit library for ASP.NET Core. 16 | 17 | true 18 | true 19 | Apache-2.0 20 | true 21 | 3.0.0.0 22 | 3.0.0.0 23 | 24 | 25 | 26 | 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | all 29 | 30 | 31 | 32 | 33 | 34 | 35 | README.md 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNet/HttpRateLimitError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using FireflySoft.RateLimit.Core; 6 | 7 | namespace FireflySoft.RateLimit.AspNet 8 | { 9 | /// 10 | /// Rate Limit Error 11 | /// 12 | public class HttpRateLimitError 13 | { 14 | /// 15 | /// Create a new instance 16 | /// 17 | public HttpRateLimitError() 18 | { 19 | } 20 | 21 | /// 22 | /// Get or set the http response status code. 23 | /// 24 | /// 25 | public int HttpStatusCode { get; set; } = 429; 26 | 27 | /// 28 | /// A delegates that defines from which response headers are builded. 29 | /// 30 | /// 31 | public Func> BuildHttpHeaders { get; set; } 32 | 33 | /// 34 | /// A delegates that defines from which response content are builded. 35 | /// 36 | /// 37 | public Func BuildHttpContent { get; set; } 38 | 39 | /// 40 | /// A delegates that defines from which response headers are builded. 41 | /// 42 | /// 43 | public Func>> BuildHttpHeadersAsync { get; set; } 44 | 45 | /// 46 | /// A delegates that defines from which response content are builded. 47 | /// 48 | /// 49 | public Func> BuildHttpContentAsync { get; set; } 50 | } 51 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/RuleCheckResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FireflySoft.RateLimit.Core.Rule; 3 | 4 | namespace FireflySoft.RateLimit.Core 5 | { 6 | /// 7 | /// Defines the result of single rule check 8 | /// 9 | public class RuleCheckResult 10 | { 11 | /// 12 | /// If true, it means that the current request should be limited 13 | /// 14 | /// 15 | public bool IsLimit { get; set; } 16 | 17 | /// 18 | /// The time to open the next time window, 19 | /// or the time when the rate limiting lock ends. 20 | /// 21 | /// 22 | public DateTimeOffset ResetTime { get; set; } 23 | 24 | /// 25 | /// The number of requests passed in the current time window. 26 | /// 27 | /// 28 | public long Count { get; set; } 29 | 30 | /// 31 | /// The number of requests remaining in the current time window that will not be limited. 32 | /// 33 | /// 34 | /// 35 | public long Remaining { get; set; } 36 | 37 | /// 38 | /// The queue waiting time of the current request, which is only for the leaky bucket algorithm. 39 | /// With Task.Dealy, you can simulate queue processing requests. 40 | /// 41 | /// 42 | public long Wait { get; set; } = -1; 43 | 44 | /// 45 | /// The current rate limit target 46 | /// 47 | /// 48 | public string Target { get; set; } 49 | 50 | /// 51 | /// The current rule 52 | /// 53 | /// 54 | public RateLimitRule Rule { get; set; } 55 | } 56 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/CounterDictionaryTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using FireflySoft.RateLimit.Core.InProcessAlgorithm; 5 | using FireflySoft.RateLimit.Core.Time; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | namespace FireflySoft.RateLimit.Core.Test 9 | { 10 | [TestClass] 11 | public class CounterDictionaryTest 12 | { 13 | [DataTestMethod] 14 | public void TryGet_ExpiredItem_ReturnFalse() 15 | { 16 | var stubTimeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(1)); 17 | CounterDictionary dic = new CounterDictionary(stubTimeProvider); 18 | 19 | dic.Set("key", new CounterDictionaryItem("key", "value") 20 | { 21 | ExpireTime = DateTimeOffset.Parse("2022-01-01T00:00:20+00:00") 22 | }); 23 | stubTimeProvider.IncrementSeconds(21); 24 | 25 | bool result = dic.TryGet("key", out CounterDictionaryItem value); 26 | 27 | // run ScanForExpiredItems 28 | Thread.Sleep(10); 29 | 30 | Assert.AreEqual(false, result); 31 | } 32 | 33 | [DataTestMethod] 34 | public void TryGet_NotExpiredItem_ReturnTrue() 35 | { 36 | var stubTimeProvider = new TestTimeProvider(TimeSpan.FromMilliseconds(1)); 37 | CounterDictionary dic = new CounterDictionary(stubTimeProvider); 38 | 39 | dic.Set("key", new CounterDictionaryItem("key", "value") 40 | { 41 | ExpireTime = DateTimeOffset.Parse("2022-01-01T00:00:20+00:00") 42 | }); 43 | stubTimeProvider.IncrementSeconds(10); 44 | 45 | bool result = dic.TryGet("key", out CounterDictionaryItem value); 46 | 47 | Assert.AreEqual(true, result); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/RateLimit/RateLimitRuleDAO.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Data; 3 | using Dapper; 4 | using MySql.Data.MySqlClient; 5 | 6 | namespace RuleAutoUpdate.RateLimit; 7 | 8 | /* 9 | First execute the sql in mysql. 10 | Then add 'DbConn' section in appsettings.json. 11 | 12 | CREATE TABLE `rate_limit_rule` ( 13 | `Id` varchar(40) NOT NULL, 14 | `Path` varchar(100) NOT NULL, 15 | `PathType` int(11) NOT NULL, 16 | `TokenCapacity` int(11) NOT NULL, 17 | `TokenSpeed` int(11) NOT NULL, 18 | `AddTime` datetime NOT NULL, 19 | `UpdateTime` datetime NOT NULL, 20 | PRIMARY KEY (`Id`) 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 22 | 23 | INSERT INTO rate_limit_rule (Id,`Path`,PathType,TokenCapacity,TokenSpeed,AddTime,UpdateTime) VALUES 24 | ('1','/WeatherForecast/GetToday',1,26,20,'2021-11-16 00:00:00.0','2021-11-16 00:00:00.0'), 25 | ('2','/WeatherForecast/GetTomorrow',1,13,10,'2021-11-16 00:00:00.0','2021-11-16 00:00:00.0'), 26 | ('3','All',2,29,25,'2021-11-16 00:00:00.0','2021-11-16 00:00:00.0'); 27 | */ 28 | 29 | public class RateLimitRule 30 | { 31 | public string? Id { get; set; } 32 | public string? Path { get; set; } 33 | public LimitPathType PathType { get; set; } 34 | public int TokenCapacity { get; set; } 35 | public int TokenSpeed { get; set; } 36 | public DateTime AddTime { get; set; } 37 | public DateTime UpdateTime { get; set; } 38 | } 39 | 40 | public class RateLimitRuleDAO 41 | { 42 | private readonly IConfiguration _configuration; 43 | 44 | public RateLimitRuleDAO(IConfiguration configuration) 45 | { 46 | _configuration = configuration; 47 | } 48 | 49 | public async Task> GetAllRulesAsync() 50 | { 51 | var conn = _configuration.GetValue("DbConn"); 52 | using (IDbConnection db = new MySqlConnection(conn)) 53 | { 54 | return await db.QueryAsync("select * from rate_limit_rule"); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Time/LocalTimeProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace FireflySoft.RateLimit.Core.Time 5 | { 6 | /// 7 | /// Local time provider 8 | /// 9 | public class LocalTimeProvider : ITimeProvider 10 | { 11 | /// 12 | /// Get current utc time 13 | /// 14 | /// 15 | public DateTimeOffset GetCurrentUtcTime() 16 | { 17 | return DateTimeOffset.UtcNow; 18 | } 19 | 20 | /// 21 | /// Get the milliseconds of current unix time 22 | /// 23 | /// 24 | public long GetCurrentUtcMilliseconds() 25 | { 26 | return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); 27 | } 28 | 29 | /// 30 | /// Get current local time 31 | /// 32 | /// 33 | public DateTimeOffset GetCurrentLocalTime() 34 | { 35 | return DateTimeOffset.Now; 36 | } 37 | 38 | /// 39 | /// Get current utc time 40 | /// 41 | /// 42 | public async Task GetCurrentUtcTimeAsync() 43 | { 44 | return await Task.FromResult(GetCurrentUtcTime()).ConfigureAwait(false); 45 | } 46 | 47 | /// 48 | /// Get the milliseconds of current unix time 49 | /// 50 | /// 51 | public async Task GetCurrentUtcMillisecondsAsync() 52 | { 53 | return await Task.FromResult(GetCurrentUtcMilliseconds()).ConfigureAwait(false); 54 | } 55 | 56 | /// 57 | /// Get current local time 58 | /// 59 | /// 60 | public async Task GetCurrentLocalTimeAsync() 61 | { 62 | return await Task.FromResult(GetCurrentLocalTime()).ConfigureAwait(false); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNetCore/HttpErrorResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FireflySoft.RateLimit.Core; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Primitives; 7 | 8 | namespace FireflySoft.RateLimit.AspNetCore 9 | { 10 | /// 11 | /// Defines the http error response for rate limit 12 | /// 13 | public class HttpErrorResponse 14 | { 15 | /// 16 | /// Create a new instance 17 | /// 18 | public HttpErrorResponse() 19 | { 20 | } 21 | 22 | /// 23 | /// Get or set the http response status code. 24 | /// 25 | /// 26 | public int HttpStatusCode { get; set; } = 429; 27 | 28 | /// 29 | /// A delegates that defines from which response headers are builded. 30 | /// Asynchronous method is preferred, and this method is used when asynchronous method does not exist. 31 | /// 32 | /// 33 | public Func> BuildHttpHeaders { get; set; } 34 | 35 | /// 36 | /// A delegates that defines from which response content are builded. 37 | /// Asynchronous method is preferred, and this method is used when asynchronous method does not exist. 38 | /// 39 | /// 40 | public Func BuildHttpContent { get; set; } 41 | 42 | /// 43 | /// A delegates that defines from which response headers are builded. 44 | /// 45 | /// 46 | public Func>> BuildHttpHeadersAsync { get; set; } 47 | 48 | /// 49 | /// A delegates that defines from which response content are builded. 50 | /// 51 | /// 52 | public Func> BuildHttpContentAsync { get; set; } 53 | } 54 | } -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/RateLimit/RateLimitConfigurationManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace RuleAutoUpdate.RateLimit; 4 | 5 | public class RateLimitConfiguration 6 | { 7 | public string? Path { get; set; } 8 | public LimitPathType PathType { get; set; } 9 | public int TokenCapacity { get; set; } 10 | public int TokenSpeed { get; set; } 11 | } 12 | 13 | public enum LimitPathType 14 | { 15 | Single = 1, 16 | All = 2 17 | } 18 | 19 | public class RateLimitConfiguratioinManager 20 | { 21 | readonly RateLimitRuleDAO _dao; 22 | readonly Timer _checkConfigChangedTimer; 23 | DateTime _lastConfigChangedTime; 24 | 25 | Action>? _action; 26 | 27 | public RateLimitConfiguratioinManager(RateLimitRuleDAO dao) 28 | { 29 | _dao = dao; 30 | _lastConfigChangedTime = DateTime.MinValue; 31 | _checkConfigChangedTimer = NonCapturingTimer.Create(new TimerCallback(CheckConfigChangedTimerCallbackAsync), this, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(10)); 32 | } 33 | 34 | public void Watch(Action> action) 35 | { 36 | _action = action; 37 | } 38 | 39 | private async void CheckConfigChangedTimerCallbackAsync(object? state) 40 | { 41 | var rules = await _dao.GetAllRulesAsync(); 42 | if (rules.Any()) 43 | { 44 | var latestChangedTime = rules.OrderByDescending(d => d.UpdateTime).Select(d => d.UpdateTime).First(); 45 | if (latestChangedTime > _lastConfigChangedTime) 46 | { 47 | 48 | var configs = rules.Select(d => 49 | { 50 | return new RateLimitConfiguration() 51 | { 52 | Path = d.Path, 53 | PathType = d.PathType, 54 | TokenCapacity = d.TokenCapacity, 55 | TokenSpeed = d.TokenSpeed 56 | }; 57 | }); 58 | 59 | _action?.Invoke(configs); 60 | 61 | _lastConfigChangedTime = latestChangedTime; 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/TestTimeProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FireflySoft.RateLimit.Core.Time; 4 | 5 | namespace FireflySoft.RateLimit.Core.Test 6 | { 7 | internal class TestTimeProvider : ITimeProvider 8 | { 9 | private DateTimeOffset _startTime; 10 | 11 | private TimeSpan _interval; 12 | 13 | public TestTimeProvider(TimeSpan interval) 14 | { 15 | _startTime = DateTimeOffset.Parse("2022-01-01T00:00:00+00:00"); 16 | _interval = interval; 17 | } 18 | 19 | public TestTimeProvider(DateTimeOffset startTime, TimeSpan interval) 20 | { 21 | _startTime = startTime; 22 | _interval = interval; 23 | } 24 | 25 | public long GetCurrentUtcMilliseconds() 26 | { 27 | return _startTime.ToUnixTimeMilliseconds(); 28 | } 29 | 30 | public DateTimeOffset GetCurrentUtcTime() 31 | { 32 | return _startTime; 33 | } 34 | 35 | public DateTimeOffset GetCurrentLocalTime() 36 | { 37 | return _startTime.ToLocalTime(); 38 | } 39 | 40 | public Task GetCurrentUtcMillisecondsAsync() 41 | { 42 | return Task.FromResult(GetCurrentUtcMilliseconds()); 43 | } 44 | 45 | public Task GetCurrentLocalTimeAsync() 46 | { 47 | return Task.FromResult(GetCurrentLocalTime()); 48 | } 49 | 50 | public Task GetCurrentUtcTimeAsync() 51 | { 52 | return Task.FromResult(GetCurrentUtcTime()); 53 | } 54 | 55 | public void Increment() 56 | { 57 | _startTime = _startTime.Add(_interval); 58 | } 59 | 60 | public void Increment(TimeSpan interval) 61 | { 62 | _startTime = _startTime.Add(interval); 63 | } 64 | 65 | public void IncrementMilliseconds(int milliseconds) 66 | { 67 | Increment(TimeSpan.FromMilliseconds(milliseconds)); 68 | } 69 | 70 | public void IncrementSeconds(int seconds) 71 | { 72 | Increment(TimeSpan.FromSeconds(seconds)); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/LocalTimeProviderTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using FireflySoft.RateLimit.Core.Time; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | namespace FireflySoft.RateLimit.Core.Test 8 | { 9 | [TestClass] 10 | public class LocalTimeProviderTest 11 | { 12 | [DataTestMethod] 13 | public void TestGetCurrentUtcTime() 14 | { 15 | var currentUtcTime = GetTimeProvider().GetCurrentUtcTime(); 16 | Assert.AreEqual(true, currentUtcTime <= DateTimeOffset.UtcNow); 17 | } 18 | 19 | [DataTestMethod] 20 | public void TestGetCurrentUtcMilliseconds() 21 | { 22 | var currentTs = GetTimeProvider().GetCurrentUtcMilliseconds(); 23 | Assert.AreEqual(true, currentTs <= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); 24 | } 25 | 26 | [DataTestMethod] 27 | public void TestGetCurrentLocalTime() 28 | { 29 | var localTime = GetTimeProvider().GetCurrentLocalTime(); 30 | Assert.AreEqual(true, localTime <= DateTimeOffset.Now); 31 | } 32 | 33 | [DataTestMethod] 34 | public async Task TestGetCurrentUtcTimeAsync() 35 | { 36 | var currentUtcTime = await GetTimeProvider().GetCurrentUtcTimeAsync(); 37 | Assert.AreEqual(true, currentUtcTime.Offset == TimeSpan.FromHours(0)); 38 | Assert.AreEqual(true, currentUtcTime <= DateTimeOffset.UtcNow); 39 | } 40 | 41 | [DataTestMethod] 42 | public async Task TestGetCurrentUtcMillisecondsAsync() 43 | { 44 | var currentTs = await GetTimeProvider().GetCurrentUtcMillisecondsAsync(); 45 | Assert.AreEqual(true, currentTs <= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); 46 | } 47 | 48 | [DataTestMethod] 49 | public async Task TestGetCurrentLocalTimeAsync() 50 | { 51 | var localTime = await GetTimeProvider().GetCurrentLocalTimeAsync(); 52 | Assert.AreEqual(true, localTime <= DateTimeOffset.Now); 53 | } 54 | 55 | private ITimeProvider GetTimeProvider() 56 | { 57 | return new LocalTimeProvider(); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/RedisTimeProviderTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using FireflySoft.RateLimit.Core.Time; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | namespace FireflySoft.RateLimit.Core.Test 8 | { 9 | [TestClass] 10 | public class RedisTimeProviderTest 11 | { 12 | [DataTestMethod] 13 | public void TestGetCurrentUtcTime() 14 | { 15 | var currentUtcTime = GetTimeProvider().GetCurrentUtcTime(); 16 | Assert.AreEqual(true, currentUtcTime.Year >= 2021); 17 | } 18 | 19 | [DataTestMethod] 20 | public void TestGetCurrentUtcMilliseconds() 21 | { 22 | var currentTs = GetTimeProvider().GetCurrentUtcMilliseconds(); 23 | Assert.AreEqual(true, currentTs > DateTimeOffset.Parse("2021-1-1").ToUnixTimeMilliseconds()); 24 | } 25 | 26 | [DataTestMethod] 27 | public void TestGetCurrentLocalTime() 28 | { 29 | var localTime = GetTimeProvider().GetCurrentLocalTime(); 30 | Assert.AreEqual(true, localTime.Year >= 2021); 31 | } 32 | 33 | [DataTestMethod] 34 | public async Task TestGetCurrentUtcTimeAsync() 35 | { 36 | var currentUtcTime = await GetTimeProvider().GetCurrentUtcTimeAsync(); 37 | Assert.AreEqual(true, currentUtcTime.Offset == TimeSpan.FromHours(0)); 38 | Assert.AreEqual(true, currentUtcTime.Year >= 2021); 39 | } 40 | 41 | [DataTestMethod] 42 | public async Task TestGetCurrentUtcMillisecondsAsync() 43 | { 44 | var currentTs = await GetTimeProvider().GetCurrentUtcMillisecondsAsync(); 45 | Assert.AreEqual(true, currentTs > DateTimeOffset.Parse("2021-1-1").ToUnixTimeMilliseconds()); 46 | } 47 | 48 | [DataTestMethod] 49 | public async Task TestGetCurrentLocalTimeAsync() 50 | { 51 | var localTime = await GetTimeProvider().GetCurrentLocalTimeAsync(); 52 | Assert.AreEqual(true, localTime.Year >= 2021); 53 | } 54 | 55 | private ITimeProvider GetTimeProvider() 56 | { 57 | return new RedisTimeProvider(RedisClientHelper.GetClient()); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/FireflySoft.RateLimit.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | FireflySoft.RateLimit.Core 6 | 3.0.1 7 | bossma 8 | https://github.com/bosima/FireflySoft.RateLimit 9 | 10 | 1. Break change: Modify the Count returned by the token bucket algorithm to the cumulative number of visits in the current time window to be consistent with other algorithms. 11 | 2. Add a property 'Maintaining' to 'RuleCheckResult', which represents the number of remaining allowed requests in the current time window. 12 | 3. Add a property 'ResetTime' to 'RuleCheckResult', which represents the next reset time of the rate limit time window of the current algorithm. 13 | 4. Some other optimizations. 14 | 15 | Rate Limit;Fixed Window;Sliding Window;Leaky Bucket;Token Bucket 16 | 17 | It is a rate limiting library based on .Net standard. 18 | 19 | true 20 | true 21 | Apache-2.0 22 | 3.0.1.0 23 | 3.0.1.0 24 | 25 | 26 | README.md 27 | 28 | 29 | 30 | 31 | 32 | 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | all 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Rule/TokenBucketRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FireflySoft.RateLimit.Core.Rule 5 | { 6 | /// 7 | /// The rule of token bucket algorithm 8 | /// 9 | public class TokenBucketRule : RateLimitRule 10 | { 11 | /// 12 | /// the capacity of token bucket 13 | /// 14 | public int Capacity { get; private set; } 15 | 16 | /// 17 | /// The inflow quantity per unit time 18 | /// 19 | public int InflowQuantityPerUnit { get; private set; } 20 | 21 | /// 22 | /// The time unit of inflow to the token bucket 23 | /// 24 | public TimeSpan InflowUnit { get; private set; } 25 | 26 | /// 27 | /// The min time of fill to the full. 28 | /// 29 | /// 30 | public TimeSpan MinFillTime { get; private set; } 31 | 32 | /// 33 | /// create a new instance 34 | /// 35 | /// 36 | /// 37 | /// 38 | public TokenBucketRule(int capacity, int inflowQuantityPerUnit, TimeSpan inflowUnit) 39 | { 40 | if (capacity < 1) 41 | { 42 | throw new ArgumentException("the capacity can not less than 1."); 43 | } 44 | 45 | if (inflowQuantityPerUnit < 1) 46 | { 47 | throw new ArgumentException("the inflow quantity per unit can not less than 1."); 48 | } 49 | 50 | if (inflowUnit.TotalMilliseconds < 1) 51 | { 52 | throw new ArgumentException("the inflow unit can not less than 1ms."); 53 | } 54 | 55 | Capacity = capacity; 56 | InflowQuantityPerUnit = inflowQuantityPerUnit; 57 | InflowUnit = inflowUnit; 58 | MinFillTime = TimeSpan.FromMilliseconds(((int)Math.Ceiling(capacity / (double)inflowQuantityPerUnit) + 1) * inflowUnit.TotalMilliseconds); 59 | } 60 | 61 | /// 62 | /// Get the rate limit threshold. 63 | /// 64 | /// 65 | public override long GetLimitThreshold() 66 | { 67 | return Capacity; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Rule/RateLimitRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace FireflySoft.RateLimit.Core.Rule 5 | { 6 | /// 7 | /// The rule of rate limit 8 | /// 9 | public abstract class RateLimitRule 10 | { 11 | /// 12 | /// The identity of the rule, required and cannot be duplicated within the storage currently in use. 13 | /// The default value is a Guid string. 14 | /// 15 | public string Id { get; set; } = Guid.NewGuid().ToString(); 16 | 17 | /// 18 | /// The name of the rule. 19 | /// 20 | public string Name { get; set; } 21 | 22 | /// 23 | /// The number of seconds locked after triggering rate limiting. 0 means not locked. 24 | /// 25 | public int LockSeconds { get; set; } 26 | 27 | /// 28 | /// The type of statistics start time 29 | /// 30 | public StartTimeType StartTimeType { get; set; } = StartTimeType.FromCurrent; 31 | 32 | /// 33 | /// Extract the rate limit target from the instance of T, such as a value in HTTP Header. A fixed value can be returned to restrict the access of all users. 34 | /// 35 | public Func ExtractTarget { get; set; } 36 | 37 | /// 38 | /// Extract the rate limit target from the instance of T, such as a value in HTTP Header. A fixed value can be returned to restrict the access of all users. 39 | /// 40 | public Func> ExtractTargetAsync { get; set; } 41 | 42 | /// 43 | /// Check whether the instance of T matches the rules. For example, you can check the path and HTTP Header in HttpContext. 44 | /// 45 | public Func CheckRuleMatching { get; set; } 46 | 47 | /// 48 | /// Check whether the instance of T matches the rules. For example, you can check the path and HTTP Header in HttpContext. 49 | /// 50 | public Func> CheckRuleMatchingAsync { get; set; } 51 | 52 | /// 53 | /// Get the rate limit threshold. 54 | /// 55 | /// 56 | public abstract long GetLimitThreshold(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNetCore/README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | Fireflysoft.RateLimit is a rate limiting library based on .Net standard. Its core is simple and lightweight, and can flexibly meet the rate limiting needs of many scenarios. 3 | 4 | ## Features 5 | * Multiple rate limiting algorithms: built-in fixed window, sliding window, leaky bucket, token bucket, and can be extended. 6 | * Multiple counting storage: memory and Redis (including cluster). 7 | * Distributed friendly: supports unified counting of distributed programs with Redis storage. 8 | * Flexible rate limiting targets: each data can be extracted from the request to set rate limiting targets. 9 | * Support rate limit penalty: the client can be locked for a period of time after the rate limit is triggered. 10 | * Time window enhancement: support to the millisecond level; support starting from the starting point of time periods such as seconds, minutes, hours, dates, etc. 11 | * Real-time tracking: the number of requests processed and the remaining allowed requests in the current counting cycle, as well as the reset time of the counting cycle. 12 | * Dynamically change the rules: support the dynamic change of the rate limiting rules when the program is running. 13 | * Custom error: you can customize the error code and error message after the current limit is triggered. 14 | * Universality: in principle, it can meet any scenario that requires rate limiting. 15 | 16 | ## Usage 17 | 18 | The following code calls the rate-limit [middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1) from Startup.Configure: 19 | 20 | ```csharp 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | ... 24 | 25 | services.AddRateLimit(new InProcessFixedWindowAlgorithm( 26 | new[] { 27 | new FixedWindowRule() 28 | { 29 | ExtractTarget = context => 30 | { 31 | return (context as HttpContext).Request.Path.Value; 32 | }, 33 | CheckRuleMatching = context => 34 | { 35 | return true; 36 | }, 37 | Name="default limit rule", 38 | LimitNumber=30, 39 | StatWindow=TimeSpan.FromSeconds(1) 40 | } 41 | }) 42 | ); 43 | 44 | ... 45 | } 46 | 47 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 48 | { 49 | ... 50 | 51 | app.UseRateLimit(); 52 | 53 | ... 54 | } 55 | ``` -------------------------------------------------------------------------------- /samples/aspnet/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/AlgorithmStartTimeTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FireflySoft.RateLimit.Core.Time; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace FireflySoft.RateLimit.Core.Test 6 | { 7 | [TestClass] 8 | public class AlgorithmStartTimeTest 9 | { 10 | [DataTestMethod] 11 | public void TestToNaturalPeriodBeignTime() 12 | { 13 | DateTimeOffset startTime = DateTimeOffset.Parse("2021-12-21 21:21:21.211"); 14 | 15 | var startTime1 = AlgorithmStartTime.ToNaturalPeriodBeignTime(startTime, TimeSpan.Parse("1")); 16 | Assert.AreEqual("2021-12-21 00:00:00.000", startTime1.ToString("yyyy-MM-dd HH:mm:ss.fff")); 17 | 18 | var startTime2 = AlgorithmStartTime.ToNaturalPeriodBeignTime(startTime, TimeSpan.Parse("0.01:00:00")); 19 | Assert.AreEqual("2021-12-21 21:00:00.000", startTime2.ToString("yyyy-MM-dd HH:mm:ss.fff")); 20 | 21 | var startTime3 = AlgorithmStartTime.ToNaturalPeriodBeignTime(startTime, TimeSpan.Parse("0.00:01:00")); 22 | Assert.AreEqual("2021-12-21 21:21:00.000", startTime3.ToString("yyyy-MM-dd HH:mm:ss.fff")); 23 | 24 | var startTime4 = AlgorithmStartTime.ToNaturalPeriodBeignTime(startTime, TimeSpan.Parse("0.00:00:01")); 25 | Assert.AreEqual("2021-12-21 21:21:21.000", startTime4.ToString("yyyy-MM-dd HH:mm:ss.fff")); 26 | } 27 | 28 | [DataTestMethod] 29 | public void TestToSpecifiedTypeTime() 30 | { 31 | DateTimeOffset startTime = DateTimeOffset.Parse("2021-12-21 21:21:21.211"); 32 | long startTimeTs = startTime.ToUnixTimeMilliseconds(); 33 | 34 | var startTime1 = AlgorithmStartTime.ToSpecifiedTypeTime(startTime, TimeSpan.Parse("1"), StartTimeType.FromCurrent); 35 | Assert.AreEqual("2021-12-21 21:21:21.211", startTime1.ToString("yyyy-MM-dd HH:mm:ss.fff")); 36 | 37 | var startTime2 = AlgorithmStartTime.ToSpecifiedTypeTime(startTime, TimeSpan.Parse("1"), StartTimeType.FromNaturalPeriodBeign); 38 | Assert.AreEqual("2021-12-21 00:00:00.000", startTime2.ToString("yyyy-MM-dd HH:mm:ss.fff")); 39 | 40 | var startTime3 = AlgorithmStartTime.ToSpecifiedTypeTime(startTimeTs, TimeSpan.Parse("1"), StartTimeType.FromCurrent); 41 | Assert.AreEqual(startTimeTs, startTime3); 42 | 43 | var startTime4 = AlgorithmStartTime.ToSpecifiedTypeTime(startTimeTs, TimeSpan.Parse("1"), StartTimeType.FromNaturalPeriodBeign); 44 | Assert.AreEqual(1640044800000, startTime4); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/AlgorithmCheckResultTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FireflySoft.RateLimit.Core.Rule; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | namespace FireflySoft.RateLimit.Core.Test 8 | { 9 | [TestClass] 10 | public class AlgorithmCheckResultTest 11 | { 12 | [DataTestMethod] 13 | public void TestIsLimit() 14 | { 15 | Assert.AreEqual(false, result.IsLimit); 16 | } 17 | 18 | [DataTestMethod] 19 | public void TestRuleCheckResults() 20 | { 21 | for (int k = 0; k < 30; k++) 22 | { 23 | int i = 0; 24 | foreach (var r in result.RuleCheckResults) 25 | { 26 | if (i == 0) Assert.AreEqual(false, r.IsLimit); 27 | if (i == 1) Assert.AreEqual(false, r.IsLimit); 28 | i++; 29 | } 30 | } 31 | 32 | Assert.AreEqual(2, result.RuleCheckResults.Count()); 33 | } 34 | 35 | [TestInitialize()] 36 | public void TestInitialize() 37 | { 38 | List results = new List(); 39 | var checks = GetRuleCheckResults(); 40 | foreach (var c in checks) 41 | { 42 | results.Add(c); 43 | } 44 | 45 | result = new AlgorithmCheckResult(results); 46 | _countValue1 = 0; 47 | _countValue2 = 0; 48 | } 49 | 50 | private IEnumerable GetRuleCheckResults() 51 | { 52 | yield return Check("1", 10); 53 | yield return Check("2", 20); 54 | } 55 | 56 | private AlgorithmCheckResult result; 57 | private int _countValue1; 58 | private int _countValue2; 59 | 60 | private RuleCheckResult Check(string ruleId, int limit) 61 | { 62 | var _countValue = 0; 63 | if (ruleId == "1") 64 | _countValue = ++_countValue1; 65 | else 66 | _countValue = ++_countValue2; 67 | 68 | return new RuleCheckResult() 69 | { 70 | IsLimit = _countValue > limit, 71 | Count = _countValue, 72 | Target = "/home", 73 | Wait = -1, 74 | Rule = new FixedWindowRule() 75 | { 76 | Id = ruleId, 77 | Name = ruleId, 78 | StatWindow = TimeSpan.FromSeconds(1), 79 | LimitNumber = limit 80 | } 81 | }; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Rule/SlidingWindowRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FireflySoft.RateLimit.Core.Rule 5 | { 6 | /// 7 | /// The rule of sliding window algorithm 8 | /// 9 | public class SlidingWindowRule : RateLimitRule 10 | { 11 | private int _periodNumber; 12 | 13 | /// 14 | /// Gets the amount of small periods. 15 | /// 16 | /// 17 | public int PeriodNumber 18 | { 19 | get 20 | { 21 | return _periodNumber; 22 | } 23 | } 24 | 25 | /// 26 | /// Gets or sets the statistical time window, which counts the amount of requests in this time. 27 | /// 28 | public TimeSpan StatWindow { get; set; } 29 | 30 | /// 31 | /// Gets or sets the threshold of triggering rate limiting in the statistical time window. 32 | /// If less than 0, it means no limit. 33 | /// 34 | public int LimitNumber { get; set; } 35 | 36 | /// 37 | /// Gets the small period length in statistical time window. 38 | /// 39 | public TimeSpan StatPeriod { get; private set; } 40 | 41 | /// 42 | /// Create a new instance of SlidingWindowRule 43 | /// 44 | /// 45 | /// 46 | public SlidingWindowRule(TimeSpan statWindow, TimeSpan statPeriod) 47 | { 48 | if (statWindow.TotalMilliseconds < 1) 49 | { 50 | throw new ArgumentException("the stat window can not less than 1ms."); 51 | } 52 | 53 | if (statPeriod.TotalMilliseconds < 1) 54 | { 55 | throw new ArgumentException("the stat period can not less than 1ms."); 56 | } 57 | 58 | if (statWindow.TotalMilliseconds % statPeriod.TotalMilliseconds > 0) 59 | { 60 | throw new ArgumentException("The stat window must be an integral multiple of the stat period."); 61 | } 62 | 63 | StatWindow = statWindow; 64 | StatPeriod = statPeriod; 65 | 66 | _periodNumber = (int)(StatWindow.TotalMilliseconds / StatPeriod.TotalMilliseconds); 67 | } 68 | 69 | /// 70 | /// Get the rate limit threshold. 71 | /// 72 | /// 73 | public override long GetLimitThreshold() 74 | { 75 | return LimitNumber; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/README.md: -------------------------------------------------------------------------------- 1 | Github: https://github.com/bosima/FireflySoft.RateLimit 2 | 3 | ## Introduction 4 | Fireflysoft.RateLimit is a rate limiting library based on .Net standard. Its core is simple and lightweight, and can flexibly meet the rate limiting needs of many scenarios. 5 | 6 | ## Features 7 | * Multiple rate limiting algorithms: built-in fixed window, sliding window, leaky bucket, token bucket, and can be extended. 8 | * Multiple counting storage: memory and Redis (including cluster). 9 | * Distributed friendly: supports unified counting of distributed programs with Redis storage. 10 | * Flexible rate limiting targets: each data can be extracted from the request to set rate limiting targets. 11 | * Support rate limit penalty: the client can be locked for a period of time after the rate limit is triggered. 12 | * Time window enhancement: support to the millisecond level; support starting from the starting point of time periods such as seconds, minutes, hours, dates, etc. 13 | * Real-time tracking: the number of requests processed and the remaining allowed requests in the current counting cycle, as well as the reset time of the counting cycle. 14 | * Dynamically change the rules: support the dynamic change of the rate limiting rules when the program is running. 15 | * Custom error: you can customize the error code and error message after the current limit is triggered. 16 | * Universality: in principle, it can meet any scenario that requires rate limiting. 17 | 18 | ## Usage 19 | 20 | **If you need to use it in ASP.NET Core, it is recommended to install the package FireflySoft.RateLimit.AspNetCore.** 21 | 22 | Use *IAlgorithm* to filter every request, process the return value of *Check* method. 23 | 24 | ```csharp 25 | // Rule 26 | var fixedWindowRules = new FixedWindowRule[] 27 | { 28 | new FixedWindowRule() 29 | { 30 | Id = "3", 31 | StatWindow=TimeSpan.FromSeconds(1), 32 | LimitNumber=30, 33 | ExtractTarget = (request) => 34 | { 35 | return (request as SimulationRequest).RequestResource; 36 | }, 37 | CheckRuleMatching = (request) => 38 | { 39 | return true; 40 | }, 41 | } 42 | }; 43 | 44 | // Algorithm 45 | IAlgorithm algorithm = new InProcessFixedWindowAlgorithm(fixedWindowRules); 46 | 47 | // Check 48 | var result = algorithm.Check(new SimulationRequest() 49 | { 50 | RequestId = Guid.NewGuid().ToString(), 51 | RequestResource = "home", 52 | Parameters = new Dictionary() { 53 | { "from","sample" }, 54 | } 55 | }); 56 | ``` 57 | 58 | SimulationRequest is a custom request that you can modify to any type. -------------------------------------------------------------------------------- /samples/RuleAutoUpdate/RateLimit/AutoUpdateAlgorithmManager.cs: -------------------------------------------------------------------------------- 1 | using FireflySoft.RateLimit.Core; 2 | using FireflySoft.RateLimit.Core.InProcessAlgorithm; 3 | using FireflySoft.RateLimit.Core.Rule; 4 | 5 | namespace RuleAutoUpdate.RateLimit; 6 | 7 | public class AutoUpdateAlgorithmManager 8 | { 9 | readonly RateLimitConfiguratioinManager _configurationManager; 10 | readonly IAlgorithm _algorithm; 11 | 12 | public AutoUpdateAlgorithmManager(RateLimitConfiguratioinManager configurationManager) 13 | { 14 | _configurationManager = configurationManager; 15 | _algorithm = new InProcessTokenBucketAlgorithm(new TokenBucketRule[0],updatable:true); 16 | _configurationManager.Watch(UpdateAlgorithmRules); 17 | } 18 | 19 | public IAlgorithm GetAlgorithmInstance() 20 | { 21 | return _algorithm; 22 | } 23 | 24 | private void UpdateAlgorithmRules(IEnumerable configurations) 25 | { 26 | var rules = ConvertConfigurationsToRules(configurations); 27 | _algorithm.UpdateRules(rules); 28 | } 29 | 30 | private IEnumerable ConvertConfigurationsToRules(IEnumerable configurations) 31 | { 32 | List ruleList = new List(); 33 | foreach (var configuration in configurations) 34 | { 35 | var tokenRule = new TokenBucketRule(configuration.TokenCapacity, configuration.TokenSpeed, TimeSpan.FromSeconds(1)) 36 | { 37 | ExtractTarget = context => 38 | { 39 | var requestSymbol = ExtractRequestSymbol((HttpContext)context); 40 | 41 | return configuration.PathType == LimitPathType.Single ? 42 | requestSymbol.Item1 + "," + requestSymbol.Item2 : 43 | requestSymbol.Item1; 44 | }, 45 | CheckRuleMatching = context => 46 | { 47 | var requestSymbol = ExtractRequestSymbol((HttpContext)context); 48 | 49 | return configuration.PathType == LimitPathType.Single ? 50 | configuration.Path == requestSymbol.Item2 : 51 | !string.IsNullOrWhiteSpace(requestSymbol.Item1); 52 | }, 53 | Name = $"The Rule for '{configuration.Path}'", 54 | }; 55 | ruleList.Add(tokenRule); 56 | } 57 | 58 | return ruleList; 59 | } 60 | 61 | public Tuple ExtractRequestSymbol(HttpContext httpContext) 62 | { 63 | var requestUserId = httpContext.Request.Query["UserId"].FirstOrDefault(); 64 | var requestPath = httpContext.Request.Path.Value; 65 | return new Tuple(requestUserId, requestPath); 66 | } 67 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Rule/LeakyBucketRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace FireflySoft.RateLimit.Core.Rule 5 | { 6 | /// 7 | /// The rule of leaky bucket algorithm 8 | /// 9 | public class LeakyBucketRule : RateLimitRule 10 | { 11 | /// 12 | /// The capacity of current leaky bucket 13 | /// 14 | public long Capacity { get; private set; } 15 | 16 | /// 17 | /// The outflow quantity per unit time 18 | /// 19 | public long OutflowQuantityPerUnit { get; private set; } 20 | 21 | /// 22 | /// The time unit of outflow from the leaky bucket 23 | /// 24 | public TimeSpan OutflowUnit { get; private set; } 25 | 26 | /// 27 | /// The threshold of triggering rate limiting in the statistical time window. 28 | /// 29 | /// 30 | public long LimitNumber { get; private set; } 31 | 32 | /// 33 | /// The length of drain time. 34 | /// 35 | /// 36 | public TimeSpan MaxDrainTime { get; private set; } 37 | 38 | /// 39 | /// create a new instance 40 | /// 41 | /// 42 | /// 43 | /// 44 | public LeakyBucketRule(long capacity, long outflowQuantityPerUnit, TimeSpan outflowUnit) 45 | { 46 | if (capacity < 1) 47 | { 48 | throw new ArgumentException("the capacity can not less than 1."); 49 | } 50 | 51 | if (outflowQuantityPerUnit < 1) 52 | { 53 | throw new ArgumentException("the outflow quantity per unit can not less than 1."); 54 | } 55 | 56 | if (outflowUnit.TotalMilliseconds < 1) 57 | { 58 | throw new ArgumentException("the outflow unit can not less than 1ms."); 59 | } 60 | 61 | Capacity = capacity; 62 | OutflowQuantityPerUnit = outflowQuantityPerUnit; 63 | OutflowUnit = outflowUnit; 64 | LimitNumber = capacity + outflowQuantityPerUnit; 65 | MaxDrainTime = TimeSpan.FromMilliseconds(((int)Math.Ceiling(capacity / (double)outflowQuantityPerUnit) + 1) * outflowUnit.TotalMilliseconds); 66 | } 67 | 68 | /// 69 | /// Get the rate limit threshold. 70 | /// 71 | /// 72 | public override long GetLimitThreshold() 73 | { 74 | return LimitNumber; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build aspnetcore", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/samples/aspnetcore/aspnetcore.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile", 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "label": "build aspnetcore6", 22 | "command": "dotnet", 23 | "type": "process", 24 | "args": [ 25 | "build", 26 | "${workspaceFolder}/samples/aspnetcore6/aspnetcore6.csproj", 27 | "/property:GenerateFullPaths=true", 28 | "/consoleloggerparameters:NoSummary" 29 | ], 30 | "problemMatcher": "$msCompile" 31 | }, 32 | { 33 | "label": "build console", 34 | "command": "dotnet", 35 | "type": "process", 36 | "args": [ 37 | "build", 38 | "${workspaceFolder}/samples/console/console.csproj", 39 | "/property:GenerateFullPaths=true", 40 | "/consoleloggerparameters:NoSummary" 41 | ], 42 | "problemMatcher": "$msCompile" 43 | }, 44 | { 45 | "label": "publish aspnetcore", 46 | "command": "dotnet", 47 | "type": "process", 48 | "args": [ 49 | "publish", 50 | "${workspaceFolder}/samples/aspnetcore/aspnetcore.csproj", 51 | "/property:GenerateFullPaths=true", 52 | "/consoleloggerparameters:NoSummary" 53 | ], 54 | "problemMatcher": "$msCompile" 55 | }, 56 | { 57 | "label": "publish aspnetcore6", 58 | "command": "dotnet", 59 | "type": "process", 60 | "args": [ 61 | "publish", 62 | "${workspaceFolder}/samples/aspnetcore6/aspnetcore6.csproj", 63 | "/property:GenerateFullPaths=true", 64 | "/consoleloggerparameters:NoSummary" 65 | ], 66 | "problemMatcher": "$msCompile" 67 | }, 68 | { 69 | "label": "watch aspnetcore6", 70 | "command": "dotnet", 71 | "type": "process", 72 | "args": [ 73 | "watch", 74 | "run", 75 | "${workspaceFolder}/samples/aspnetcore6/aspnetcore6.csproj", 76 | "/property:GenerateFullPaths=true", 77 | "/consoleloggerparameters:NoSummary" 78 | ], 79 | "problemMatcher": "$msCompile" 80 | } 81 | ] 82 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNetCore/HttpInvokeInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FireflySoft.RateLimit.Core; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace FireflySoft.RateLimit.AspNetCore 7 | { 8 | /// 9 | /// Http Invoke Interceptor 10 | /// 11 | public class HttpInvokeInterceptor 12 | { 13 | /// 14 | /// Do work before rate limit check 15 | /// 16 | /// 17 | public Action OnBeforeCheck { get; set; } 18 | 19 | /// 20 | /// Do work before rate limit check 21 | /// 22 | /// 23 | public Func OnBeforeCheckAsync { get; set; } 24 | 25 | /// 26 | /// Do work after rate limit check 27 | /// 28 | /// 29 | public Action OnAfterCheck { get; set; } 30 | 31 | /// 32 | /// Do work after rate limit check 33 | /// 34 | /// 35 | public Func OnAfterCheckAsync { get; set; } 36 | 37 | /// 38 | /// Do work when rate limit triggered 39 | /// 40 | /// 41 | public Action OnTriggered { get; set; } 42 | 43 | /// 44 | /// Do work when rate limit triggered 45 | /// 46 | /// 47 | public Func OnTriggeredAsync { get; set; } 48 | 49 | /// 50 | /// Do work when rate limit not triggered and before do next middleware. 51 | /// Doesn't write to the Response. 52 | /// 53 | /// 54 | public Action OnBreforUntriggeredDoNext { get; set; } 55 | 56 | /// 57 | /// Do work when rate limit not triggered and before do next middleware. 58 | /// Doesn't write to the Response. 59 | /// 60 | /// 61 | public Func OnBreforUntriggeredDoNextAsync { get; set; } 62 | 63 | /// 64 | /// Do work when rate limit not triggered and after do next middleware. 65 | /// Doesn't write to the Response. 66 | /// 67 | /// 68 | public Action OnAfterUntriggeredDoNext { get; set; } 69 | 70 | /// 71 | /// Do work when rate limit not triggered and after do next middleware. 72 | /// Doesn't write to the Response. 73 | /// 74 | /// 75 | public Func OnAfterUntriggeredDoNextAsync { get; set; } 76 | } 77 | } -------------------------------------------------------------------------------- /samples/aspnet/Web.config: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Time/RedisTimeProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using StackExchange.Redis; 4 | 5 | namespace FireflySoft.RateLimit.Core.Time 6 | { 7 | /// 8 | /// The time provider of redis 9 | /// 10 | public class RedisTimeProvider : ITimeProvider 11 | { 12 | private readonly ConnectionMultiplexer _redisClient; 13 | /// 14 | /// 15 | /// 16 | public RedisTimeProvider(ConnectionMultiplexer redisClient) 17 | { 18 | _redisClient = redisClient; 19 | } 20 | 21 | /// 22 | /// Get current utc time 23 | /// 24 | /// 25 | public DateTimeOffset GetCurrentUtcTime() 26 | { 27 | var server = GetRedisServer(); 28 | return server.Time(); 29 | } 30 | 31 | /// 32 | /// 33 | /// 34 | /// 35 | public long GetCurrentUtcMilliseconds() 36 | { 37 | DateTimeOffset utcTime = GetCurrentUtcTime(); 38 | return utcTime.ToUnixTimeMilliseconds(); 39 | } 40 | 41 | /// 42 | /// Get current local time 43 | /// 44 | /// 45 | public DateTimeOffset GetCurrentLocalTime() 46 | { 47 | DateTimeOffset utcTime = GetCurrentUtcTime(); 48 | return utcTime.ToLocalTime(); 49 | } 50 | 51 | /// 52 | /// Get current local time 53 | /// 54 | /// 55 | public async Task GetCurrentUtcTimeAsync() 56 | { 57 | var server = GetRedisServer(); 58 | return await server.TimeAsync().ConfigureAwait(false); 59 | } 60 | 61 | /// 62 | /// Get the milliseconds of current unix time 63 | /// 64 | /// 65 | public async Task GetCurrentUtcMillisecondsAsync() 66 | { 67 | DateTimeOffset utcTime = await GetCurrentUtcTimeAsync().ConfigureAwait(false); 68 | return utcTime.ToUnixTimeMilliseconds(); 69 | } 70 | 71 | /// 72 | /// Get current local time 73 | /// 74 | /// 75 | public async Task GetCurrentLocalTimeAsync() 76 | { 77 | DateTimeOffset utcTime = await GetCurrentUtcTimeAsync().ConfigureAwait(false); 78 | return utcTime.ToLocalTime(); 79 | } 80 | 81 | private IServer GetRedisServer() 82 | { 83 | var endPoints = _redisClient.GetEndPoints(); 84 | foreach (var endPoint in endPoints) 85 | { 86 | var server = _redisClient.GetServer(endPoint); 87 | if (server.IsConnected) 88 | { 89 | return server; 90 | } 91 | } 92 | 93 | throw new RedisException("could not found valid redis server."); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/Time/AlgorithmStartTime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FireflySoft.RateLimit.Core.Time 4 | { 5 | /// 6 | /// Defines some methods for the start time of algorithm 7 | /// 8 | public class AlgorithmStartTime 9 | { 10 | /// 11 | /// Convert to the time of specified type 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | public static DateTimeOffset ToSpecifiedTypeTime(DateTimeOffset startTime, TimeSpan statWindow, StartTimeType startTimeType) 18 | { 19 | if (startTimeType == StartTimeType.FromNaturalPeriodBeign) 20 | { 21 | startTime = AlgorithmStartTime.ToNaturalPeriodBeignTime(startTime, statWindow); 22 | } 23 | 24 | return startTime; 25 | } 26 | 27 | /// 28 | /// Convert to the time of specified type 29 | /// 30 | /// 31 | /// 32 | /// 33 | /// 34 | public static long ToSpecifiedTypeTime(long startTimeMilliseonds, TimeSpan statWindow, StartTimeType startTimeType) 35 | { 36 | if (startTimeType == StartTimeType.FromNaturalPeriodBeign) 37 | { 38 | DateTimeOffset startTimeUtc = DateTimeOffset.FromUnixTimeMilliseconds(startTimeMilliseonds); 39 | DateTimeOffset startTime = AlgorithmStartTime.ToNaturalPeriodBeignTime(startTimeUtc, statWindow); 40 | return startTime.ToUnixTimeMilliseconds(); 41 | } 42 | 43 | return startTimeMilliseonds; 44 | } 45 | 46 | /// 47 | /// Convert to the natural period begin of the start time 48 | /// 49 | /// 50 | /// 51 | /// 52 | public static DateTimeOffset ToNaturalPeriodBeignTime(DateTimeOffset startTime, TimeSpan statWindow) 53 | { 54 | TimeSpan offset = startTime.Offset; 55 | 56 | if (statWindow.Days > 0) 57 | { 58 | startTime = new DateTimeOffset(startTime.Year, startTime.Month, startTime.Day, 0, 0, 0, offset); 59 | } 60 | else if (statWindow.Hours > 0) 61 | { 62 | startTime = new DateTimeOffset(startTime.Year, startTime.Month, startTime.Day, startTime.Hour, 0, 0, offset); 63 | } 64 | else if (statWindow.Minutes > 0) 65 | { 66 | startTime = new DateTimeOffset(startTime.Year, startTime.Month, startTime.Day, startTime.Hour, startTime.Minute, 0, offset); 67 | } 68 | else if (statWindow.Seconds > 0) 69 | { 70 | startTime = new DateTimeOffset(startTime.Year, startTime.Month, startTime.Day, startTime.Hour, startTime.Minute, startTime.Second, offset); 71 | } 72 | 73 | return startTime; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /samples/IPOrClientId/Program.cs: -------------------------------------------------------------------------------- 1 | using FireflySoft.RateLimit.AspNetCore; 2 | using FireflySoft.RateLimit.Core.InProcessAlgorithm; 3 | using FireflySoft.RateLimit.Core.Rule; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | // Add services to the container. 8 | 9 | builder.Services.AddControllers(); 10 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 11 | builder.Services.AddEndpointsApiExplorer(); 12 | builder.Services.AddSwaggerGen(); 13 | 14 | builder.Services.AddRateLimit(new InProcessFixedWindowAlgorithm( 15 | new[] { 16 | new FixedWindowRule() 17 | { 18 | ExtractTarget = context => 19 | { 20 | var httpContext= context as HttpContext; 21 | 22 | // Through CDN 23 | var ip = httpContext!.Request.Headers["Cdn-Src-Ip"].FirstOrDefault(); 24 | if (!string.IsNullOrEmpty(ip)) 25 | return ip; 26 | 27 | // Through SLB 28 | ip = httpContext!.Request.Headers["X-Forwarded-For"].FirstOrDefault(); 29 | if (!string.IsNullOrEmpty(ip)) 30 | return ip; 31 | 32 | ip = httpContext!.Connection.RemoteIpAddress?.ToString(); 33 | return ip??"Anonymous-IP"; 34 | }, 35 | CheckRuleMatching = context => 36 | { 37 | var requestPath = (context as HttpContext)!.Request.Path.Value; 38 | if (requestPath == "/WeatherForecast/Future") 39 | { 40 | return true; 41 | } 42 | return false; 43 | }, 44 | Name = "ClientIPRule", 45 | LimitNumber = 3, 46 | StatWindow = TimeSpan.FromSeconds(1) 47 | }, 48 | new FixedWindowRule() 49 | { 50 | ExtractTarget = context => 51 | { 52 | var httpContext= context as HttpContext; 53 | var clientID = httpContext!.Request.Headers["X-ClientId"].FirstOrDefault(); 54 | if (string.IsNullOrWhiteSpace(clientID)) 55 | { 56 | clientID=Guid.NewGuid().ToString(); 57 | httpContext.Response.Headers.Append("X-ClientId",clientID); 58 | } 59 | 60 | return clientID??"Anonymous-ClientId"; 61 | }, 62 | CheckRuleMatching = context => 63 | { 64 | var requestPath = (context as HttpContext)!.Request.Path.Value; 65 | if (requestPath == "/WeatherForecast/Future") 66 | { 67 | return true; 68 | } 69 | return false; 70 | }, 71 | Name = "ClientIdRule", 72 | LimitNumber = 1, 73 | StatWindow = TimeSpan.FromSeconds(1) 74 | } 75 | }) 76 | ); 77 | 78 | var app = builder.Build(); 79 | 80 | // Configure the HTTP request pipeline. 81 | if (app.Environment.IsDevelopment()) 82 | { 83 | app.UseSwagger(); 84 | app.UseSwaggerUI(); 85 | } 86 | 87 | app.UseHttpsRedirection(); 88 | 89 | app.UseAuthorization(); 90 | 91 | app.UseRateLimit(); 92 | 93 | app.MapControllers(); 94 | 95 | app.Run(); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch console", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build console", 12 | "program": "${workspaceFolder}/samples/console/bin/Debug/netcoreapp3.1/console.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/samples/console", 15 | "stopAtEntry": false, 16 | "console": "internalConsole" 17 | }, 18 | 19 | { 20 | "name": "Launch aspnetcore", 21 | "type": "coreclr", 22 | "request": "launch", 23 | "preLaunchTask": "build aspnetcore", 24 | "program": "${workspaceFolder}/samples/aspnetcore/bin/Debug/netcoreapp3.1/aspnetcore.dll", 25 | "args": [], 26 | "cwd": "${workspaceFolder}/samples/aspnetcore/", 27 | "stopAtEntry": false, 28 | "serverReadyAction": { 29 | "action": "openExternally", 30 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "sourceFileMap": { 36 | "/Views": "${workspaceFolder}/samples/aspnetcore/Views" 37 | } 38 | }, 39 | { 40 | "name": "Launch aspnetcore6", 41 | "type": "coreclr", 42 | "request": "launch", 43 | "preLaunchTask": "build aspnetcore6", 44 | "program": "${workspaceFolder}/samples/aspnetcore6/bin/Debug/net6.0/aspnetcore6.dll", 45 | "args": [], 46 | "cwd": "${workspaceFolder}/samples/aspnetcore6/", 47 | "stopAtEntry": false, 48 | "serverReadyAction": { 49 | "action": "openExternally", 50 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 51 | }, 52 | "env": { 53 | "ASPNETCORE_ENVIRONMENT": "Development" 54 | }, 55 | "sourceFileMap": { 56 | "/Views": "${workspaceFolder}/samples/aspnetcore6/Views" 57 | } 58 | }, 59 | { 60 | "name": ".NET Core Launch (console)", 61 | "type": "coreclr", 62 | "request": "launch", 63 | "preLaunchTask": "build console", 64 | // If you have changed target frameworks, make sure to update the program path. 65 | "program": "${workspaceFolder}/samples/console/bin/Debug/netcoreapp3.1/console.dll", 66 | "args": [], 67 | "cwd": "${workspaceFolder}/samples/console", 68 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 69 | "console": "internalConsole", 70 | "stopAtEntry": false 71 | }, 72 | { 73 | "name": ".NET Core Attach", 74 | "type": "coreclr", 75 | "request": "attach", 76 | "processId": "${command:pickProcess}" 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/InProcessAlgorithm/BaseInProcessAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading.Tasks; 5 | using FireflySoft.RateLimit.Core.Rule; 6 | using FireflySoft.RateLimit.Core.Time; 7 | 8 | namespace FireflySoft.RateLimit.Core.InProcessAlgorithm 9 | { 10 | /// 11 | /// The base class for in process algorithm 12 | /// 13 | public abstract class BaseInProcessAlgorithm : BaseAlgorithm 14 | { 15 | readonly CounterDictionary _lockDictionary; 16 | 17 | /// 18 | /// Create a new instance 19 | /// 20 | /// The rate limit rules 21 | /// The time provider, it is a instance of LocalTimeProvider by default. 22 | /// If rules can be updated 23 | public BaseInProcessAlgorithm(IEnumerable rules, ITimeProvider timeProvider = null, bool updatable = false) 24 | : base(rules, timeProvider, updatable) 25 | { 26 | _lockDictionary = new CounterDictionary(_timeProvider); 27 | _lockDictionary.Set("IPMS", new CounterDictionaryItem("IMPS", true) 28 | { 29 | ExpireTime = DateTimeOffset.MaxValue 30 | }); 31 | } 32 | 33 | /// 34 | /// Lock the rate limit target until the expiration time, when triggering the rate limit rule. 35 | /// 36 | /// 37 | /// 38 | /// 39 | protected bool TryLock(string target, DateTimeOffset currentTime, TimeSpan expireTimeSpan) 40 | { 41 | var expireTime = currentTime.Add(expireTimeSpan); 42 | var key = $"{target}-lock"; 43 | _lockDictionary.Set($"{target}-lock", new CounterDictionaryItem(key, true) 44 | { 45 | ExpireTime = expireTime 46 | }); 47 | 48 | return true; 49 | } 50 | 51 | /// 52 | /// Lock the rate limit target until the expiration time, when triggering the rate limit rule. 53 | /// 54 | /// 55 | /// 56 | protected bool TryLock(string target, DateTimeOffset expireTime) 57 | { 58 | var key = $"{target}-lock"; 59 | _lockDictionary.Set(key, new CounterDictionaryItem(key, true) 60 | { 61 | ExpireTime = expireTime 62 | }); 63 | 64 | return true; 65 | } 66 | 67 | /// 68 | /// Check whether the rate limit target is locked 69 | /// 70 | /// 71 | /// 72 | /// 73 | protected bool CheckLocked(string target, out DateTimeOffset? expireTime) 74 | { 75 | expireTime = null; 76 | var key = $"{target}-lock"; 77 | if (_lockDictionary.TryGet(key, out var item)) 78 | { 79 | expireTime = item.ExpireTime; 80 | return item.Counter; 81 | } 82 | 83 | return false; 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNetCore/RateLimitService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FireflySoft.RateLimit.Core; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace FireflySoft.RateLimit.AspNetCore 6 | { 7 | /// 8 | /// Rate Limit Middleware Extensions 9 | /// 10 | public static class RateLimitServiceExtensions 11 | { 12 | /// 13 | /// Add rate limit service 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | public static IServiceCollection AddRateLimit(this IServiceCollection services, IAlgorithm algorithm, HttpErrorResponse error = null, HttpInvokeInterceptor interceptor = null) 21 | { 22 | if (algorithm == null) 23 | { 24 | throw new ArgumentNullException("The algorithm service is not registered, please use 'AddRateLimit' in 'ConfigureServices' method."); 25 | } 26 | 27 | if (error == null) 28 | { 29 | error = GetDefaultErrorResponse(); 30 | } 31 | 32 | if (interceptor == null) 33 | { 34 | interceptor = new HttpInvokeInterceptor(); 35 | } 36 | 37 | services.AddSingleton(algorithm); 38 | services.AddSingleton(error); 39 | services.AddSingleton(interceptor); 40 | return services; 41 | } 42 | 43 | /// 44 | /// Add rate limit service 45 | /// 46 | /// 47 | /// 48 | /// 49 | /// 50 | /// 51 | public static IServiceCollection AddRateLimit(this IServiceCollection services, Func algorithmProvider, Func errorProvider = null, Func interceptorProvider = null) 52 | { 53 | if (algorithmProvider == null) 54 | { 55 | throw new ArgumentNullException("The algorithm service provider is not registered, please use 'AddRateLimit' in 'ConfigureServices' method."); 56 | } 57 | 58 | if (errorProvider == null) 59 | { 60 | errorProvider = serviceProvider => GetDefaultErrorResponse(); 61 | } 62 | 63 | if (interceptorProvider == null) 64 | { 65 | interceptorProvider = serviceProvider => new HttpInvokeInterceptor(); 66 | } 67 | 68 | services.AddSingleton(algorithmProvider); 69 | services.AddSingleton(errorProvider); 70 | services.AddSingleton(interceptorProvider); 71 | return services; 72 | } 73 | 74 | 75 | private static HttpErrorResponse GetDefaultErrorResponse() 76 | { 77 | return new HttpErrorResponse() 78 | { 79 | HttpStatusCode = 429, 80 | BuildHttpContent = (context, checkResult) => 81 | { 82 | return "too many requests"; 83 | } 84 | }; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNet/RateLimitHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Threading; 3 | using System.Net.Http; 4 | using FireflySoft.RateLimit.Core; 5 | using System.Collections.Generic; 6 | using System; 7 | 8 | namespace FireflySoft.RateLimit.AspNet 9 | { 10 | /// 11 | /// 限流处理器 12 | /// 13 | public class RateLimitHandler : DelegatingHandler 14 | { 15 | private readonly IAlgorithm _algorithm; 16 | private readonly HttpRateLimitError _error; 17 | 18 | /// 19 | /// 构造函数 20 | /// 21 | /// 22 | /// 23 | public RateLimitHandler(IAlgorithm algorithm, HttpRateLimitError error = null) 24 | { 25 | if (algorithm == null) 26 | { 27 | throw new ArgumentNullException("The algorithm is null."); 28 | } 29 | 30 | if (error == null) 31 | { 32 | error = new HttpRateLimitError() 33 | { 34 | HttpStatusCode = 429, 35 | BuildHttpContent = (request, checkResult) => 36 | { 37 | return "too many requests"; 38 | } 39 | }; 40 | } 41 | 42 | _algorithm = algorithm; 43 | _error = error; 44 | } 45 | 46 | /// 47 | /// 异步发送 48 | /// 49 | /// 50 | /// 51 | /// 52 | protected async override Task SendAsync( 53 | HttpRequestMessage request, CancellationToken cancellationToken) 54 | { 55 | var checkResult = _algorithm.Check(request); 56 | 57 | if (checkResult.IsLimit) 58 | { 59 | HttpResponseMessage response = new HttpResponseMessage(); 60 | response.StatusCode = (System.Net.HttpStatusCode)_error.HttpStatusCode; 61 | 62 | Dictionary headers = null; 63 | if (_error.BuildHttpHeadersAsync != null) 64 | { 65 | headers = await _error.BuildHttpHeadersAsync(request, checkResult).ConfigureAwait(false); 66 | } 67 | else if (_error.BuildHttpHeaders != null) 68 | { 69 | headers = _error.BuildHttpHeaders(request, checkResult); 70 | } 71 | if (headers != null && headers.Count > 0) 72 | { 73 | foreach (var h in headers) 74 | { 75 | response.Headers.Add(h.Key, h.Value); 76 | } 77 | } 78 | 79 | string content = null; 80 | if (_error.BuildHttpContentAsync != null) 81 | { 82 | content = await _error.BuildHttpContentAsync(request, checkResult).ConfigureAwait(false); 83 | } 84 | else if (_error.BuildHttpContent != null) 85 | { 86 | content = _error.BuildHttpContent(request, checkResult); 87 | } 88 | if (!string.IsNullOrWhiteSpace(content)) 89 | { 90 | response.Content = new StringContent(content); 91 | } 92 | else 93 | { 94 | response.Content = new StringContent(string.Empty); 95 | } 96 | 97 | return response; 98 | } 99 | 100 | return await base.SendAsync(request, cancellationToken); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.AspNet/FireflySoft.RateLimit.AspNet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {9CBEAEB7-8BE1-4824-9F0F-98B206B3A549} 7 | Library 8 | FireflySoft.RateLimit.AspNet 9 | FireflySoft.RateLimit.AspNet 10 | v4.6.1 11 | FireflySoft.RateLimit.AspNet 12 | 2.0.0 13 | bossma 14 | https://github.com/bosima/FireflySoft.RateLimit 15 | Using new designs and APIs. 16 | ASP.NET;Rate Limit;Fixed Window;Sliding Window;Leaky Bucket;Token Bucket 17 | 18 | A rate limit library for ASP.NET. 19 | 20 | true 21 | true 22 | Apache-2.0 23 | true 24 | 25 | 26 | true 27 | full 28 | false 29 | bin\Debug 30 | DEBUG; 31 | prompt 32 | 4 33 | false 34 | 35 | 36 | true 37 | bin\Release 38 | prompt 39 | 4 40 | false 41 | bin\Release\FireflySoft.RateLimit.AspNet.xml 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ..\packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll 50 | True 51 | True 52 | 53 | 54 | ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net461\System.Security.Cryptography.Algorithms.dll 55 | True 56 | True 57 | 58 | 59 | ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll 60 | True 61 | True 62 | 63 | 64 | ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll 65 | True 66 | True 67 | 68 | 69 | ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll 70 | True 71 | True 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {EC3C2079-1284-49F1-8F38-67B5A71B0483} 82 | FireflySoft.RateLimit.Core 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/InProcessAlgorithm/InProcessSlidingWindowAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FireflySoft.RateLimit.Core.Rule; 5 | using FireflySoft.RateLimit.Core.Time; 6 | 7 | namespace FireflySoft.RateLimit.Core.InProcessAlgorithm 8 | { 9 | /// 10 | /// Define an in-process sliding window algorithm 11 | /// 12 | public class InProcessSlidingWindowAlgorithm : BaseInProcessAlgorithm 13 | { 14 | readonly CounterDictionary _slidingWindows; 15 | 16 | /// 17 | /// Create a new instance 18 | /// 19 | /// The rate limit rules 20 | /// The time provider 21 | /// If rules can be updated 22 | public InProcessSlidingWindowAlgorithm(IEnumerable rules, ITimeProvider timeProvider = null, bool updatable = false) 23 | : base(rules, timeProvider, updatable) 24 | { 25 | _slidingWindows = new CounterDictionary(_timeProvider); 26 | } 27 | 28 | /// 29 | /// check single rule for target 30 | /// 31 | /// 32 | /// 33 | /// 34 | protected override RuleCheckResult CheckSingleRule(string target, RateLimitRule rule) 35 | { 36 | var currentRule = rule as SlidingWindowRule; 37 | var amount = 1; 38 | 39 | var result = InnerCheckSingleRule(target, amount, currentRule); 40 | return new RuleCheckResult() 41 | { 42 | IsLimit = result.IsLimit, 43 | Target = target, 44 | Count = result.Count, 45 | Remaining = currentRule.GetLimitThreshold() - result.Count, 46 | Rule = rule, 47 | ResetTime = result.ResetTime, 48 | }; 49 | } 50 | 51 | /// 52 | /// check single rule for target 53 | /// 54 | /// 55 | /// 56 | /// 57 | protected override async Task CheckSingleRuleAsync(string target, RateLimitRule rule) 58 | { 59 | return await Task.FromResult(CheckSingleRule(target, rule)).ConfigureAwait(false); 60 | } 61 | 62 | private (bool IsLimit, long Count, DateTimeOffset ResetTime) InnerCheckSingleRule(string target, int amount, SlidingWindowRule currentRule) 63 | { 64 | bool locked = CheckLocked(target, out DateTimeOffset? expireTime); 65 | if (locked) 66 | { 67 | return (true, -1L, expireTime.Value); 68 | } 69 | 70 | // get current time 71 | var currentTime = _timeProvider.GetCurrentLocalTime(); 72 | var currentMilliseconds = currentTime.ToUnixTimeMilliseconds(); 73 | 74 | lock (target) 75 | { 76 | // gets or sets the sliding window for current target 77 | MemorySlidingWindow slidingWindow; 78 | CounterDictionaryItem slidingWindowItem; 79 | if (!_slidingWindows.TryGet(target, out slidingWindowItem)) 80 | { 81 | slidingWindow = new MemorySlidingWindow(currentRule); 82 | slidingWindowItem = new CounterDictionaryItem( 83 | target, 84 | slidingWindow) 85 | { 86 | ExpireTime = currentTime.AddMilliseconds(currentRule.StatWindow.TotalMilliseconds * 2) 87 | }; 88 | 89 | _slidingWindows.Set(target, slidingWindowItem); 90 | } 91 | else 92 | { 93 | slidingWindow = slidingWindowItem.Counter; 94 | } 95 | 96 | // renewal the window 97 | if (slidingWindowItem.ExpireTime < currentTime.Add(currentRule.StatWindow)) 98 | { 99 | slidingWindowItem.ExpireTime = currentTime.AddMilliseconds(currentRule.StatWindow.TotalMilliseconds * 2); 100 | } 101 | 102 | // rule changed, reset the counter 103 | slidingWindow.ResetIfRuleChanged(currentRule); 104 | 105 | // maybe replace a period, so call it first 106 | var period = slidingWindow.LoadPeriod(currentMilliseconds); 107 | var periodIndex = period.periodIndex; 108 | var periodId = period.periodId; 109 | 110 | // compare the count and the threshold 111 | var currentTotalAmount = slidingWindow.GetCount(); 112 | var totalAmount = currentTotalAmount + amount; 113 | if (currentRule.LimitNumber >= 0 && totalAmount > currentRule.LimitNumber) 114 | { 115 | if (currentRule.LockSeconds > 0) 116 | { 117 | expireTime = currentTime.AddSeconds(currentRule.LockSeconds); 118 | TryLock(target, expireTime.Value); 119 | return (true, currentTotalAmount, expireTime.Value); 120 | } 121 | else 122 | { 123 | return (true, currentTotalAmount, DateTimeOffset.FromUnixTimeMilliseconds(periodId + 1)); 124 | } 125 | } 126 | 127 | // increment the count value 128 | slidingWindow.IncreamentPeriod(periodIndex, amount); 129 | 130 | return (false, totalAmount, DateTimeOffset.FromUnixTimeMilliseconds(periodId + 1)); 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core.Test/MemorySlidingWindowTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FireflySoft.RateLimit.Core.InProcessAlgorithm; 3 | using FireflySoft.RateLimit.Core.Rule; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace FireflySoft.RateLimit.Core.Test 7 | { 8 | [TestClass] 9 | public class MemorySlidingWindowTest 10 | { 11 | [DataTestMethod] 12 | public void Increament_SameSeconds_ReturnSamePeriodIndex() 13 | { 14 | var rule = new SlidingWindowRule(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1)); 15 | MemorySlidingWindow slidingWindow = new MemorySlidingWindow(rule); 16 | for (int i = 1; i <= 1000; i++) 17 | { 18 | var (periodIndex,periodId) = slidingWindow.LoadPeriod(1637459743000); 19 | Assert.AreEqual(0, periodIndex); 20 | } 21 | } 22 | 23 | [DataTestMethod] 24 | public void Increament_MultiSeconds_ReturnNormalPeriodIndexPerSecond() 25 | { 26 | var rule = new SlidingWindowRule(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1)); 27 | MemorySlidingWindow slidingWindow = new MemorySlidingWindow(rule); 28 | 29 | long startTime = 1637459743000; 30 | for (int j = 1; j <= 10; j++) 31 | { 32 | for (int i = 1; i <= 1000; i++) 33 | { 34 | var (periodIndex,periodId) = slidingWindow.LoadPeriod(startTime); 35 | Assert.AreEqual(j - 1, periodIndex); 36 | } 37 | 38 | startTime += 1000; 39 | } 40 | } 41 | 42 | [DataTestMethod] 43 | public void Increament_SameSeconds_ReturnNormalAccum() 44 | { 45 | var rule = new SlidingWindowRule(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1)); 46 | MemorySlidingWindow slidingWindow = new MemorySlidingWindow(rule); 47 | for (int i = 1; i <= 1000; i++) 48 | { 49 | var (periodIndex,periodId) = slidingWindow.LoadPeriod(1637459743000); 50 | var countValue = slidingWindow.IncreamentPeriod(periodIndex, 1); 51 | Assert.AreEqual(i, countValue); 52 | } 53 | } 54 | 55 | [DataTestMethod] 56 | public void Increament_SameSeconds_ReturnNormalCountValue() 57 | { 58 | var rule = new SlidingWindowRule(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1)); 59 | MemorySlidingWindow slidingWindow = new MemorySlidingWindow(rule); 60 | for (int i = 1; i <= 1000; i++) 61 | { 62 | var (periodIndex,periodId) = slidingWindow.LoadPeriod(1637459743000); 63 | slidingWindow.IncreamentPeriod(periodIndex, 1); 64 | } 65 | 66 | var countValue = slidingWindow.GetCount(); 67 | Assert.AreEqual(1000, countValue); 68 | } 69 | 70 | [DataTestMethod] 71 | public void Increament_MultiSeconds_ReturnNormalAccumPerSecond() 72 | { 73 | var rule = new SlidingWindowRule(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1)); 74 | MemorySlidingWindow slidingWindow = new MemorySlidingWindow(rule); 75 | 76 | long startTime = 1637459743000; 77 | for (int j = 1; j <= 10; j++) 78 | { 79 | for (int i = 1; i <= 1000; i++) 80 | { 81 | var (periodIndex,periodId) = slidingWindow.LoadPeriod(startTime); 82 | var countValue = slidingWindow.IncreamentPeriod(periodIndex, 1); 83 | Assert.AreEqual(i, countValue); 84 | } 85 | 86 | startTime += 1000; 87 | } 88 | } 89 | 90 | [DataTestMethod] 91 | public void Increament_MultiSeconds_ReturnNormalCountValue() 92 | { 93 | var rule = new SlidingWindowRule(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1)); 94 | MemorySlidingWindow slidingWindow = new MemorySlidingWindow(rule); 95 | 96 | long startTime = 1637459743000; 97 | for (int j = 1; j <= 10; j++) 98 | { 99 | for (int i = 1; i <= 1000; i++) 100 | { 101 | var (periodIndex,periodId) = slidingWindow.LoadPeriod(startTime); 102 | slidingWindow.IncreamentPeriod(periodIndex, 1); 103 | } 104 | 105 | startTime += 1000; 106 | } 107 | 108 | var countValue = slidingWindow.GetCount(); 109 | Assert.AreEqual(10000, countValue); 110 | } 111 | 112 | [DataTestMethod] 113 | public void Increament_SkipMultiSeconds_ReturnNormalCountValue() 114 | { 115 | var rule = new SlidingWindowRule(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(1)); 116 | MemorySlidingWindow slidingWindow = new MemorySlidingWindow(rule); 117 | 118 | long startTime = 1637459743000; 119 | for (int j = 1; j <= 15; j++) 120 | { 121 | if (j <= 10) 122 | { 123 | for (int i = 1; i <= 1000; i++) 124 | { 125 | var (periodIndex,periodId) = slidingWindow.LoadPeriod(startTime); 126 | slidingWindow.IncreamentPeriod(periodIndex, 1); 127 | } 128 | } 129 | else 130 | { 131 | for (int i = 1; i <= 500; i++) 132 | { 133 | var (periodIndex,periodId) = slidingWindow.LoadPeriod(startTime); 134 | slidingWindow.IncreamentPeriod(periodIndex, 1); 135 | } 136 | } 137 | 138 | startTime += 1000; 139 | } 140 | 141 | var countValue = slidingWindow.GetCount(); 142 | Assert.AreEqual(7500, countValue); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # FireflySoft.RateLimit           [English](https://github.com/bosima/FireflySoft.RateLimit) 2 | 3 | - [FireflySoft.RateLimit           English](#fireflysoftratelimitenglish) 4 | - [介绍](#介绍) 5 | - [功能](#功能) 6 | - [项目说明](#项目说明) 7 | - [使用说明](#使用说明) 8 | - [ASP.NET Core 应用](#aspnet-core-应用) 9 | - [ASP.NET 应用](#aspnet-应用) 10 | - [其它类型应用](#其它类型应用) 11 | 12 | ## 介绍 13 | FireflySoft.RateLimit 是一个基于 .NET Standard 的限流类库,其内核简单轻巧,能够灵活应对各种需求的限流场景。 14 | 15 | ## 功能 16 | * 多种限流算法:内置固定窗口、滑动窗口、漏桶、令牌桶四种算法,还可自定义扩展。 17 | * 多种计数存储:目前支持内存、Redis(含集群)两种存储方式。 18 | * 分布式友好:通过Redis存储支持分布式程序统一计数。 19 | * 限流目标灵活:可以从请求中提取各种数据用于设置限流目标。 20 | * 支持限流惩罚:可以在客户端触发限流后锁定一段时间不允许其访问。 21 | * 时间窗口增强:支持到毫秒级别;支持从秒、分钟、小时、日期等时间周期的起始点开始。 22 | * 实时限流跟踪:当前计数周期内已处理的请求数、剩余允许请求数,以及计数周期重置的时间。 23 | * 动态更改规则:支持程序运行时动态更改限流规则。 24 | * 自定义错误:可以自定义触发限流后的错误码和错误消息。 25 | * 普适性:原则上可以满足任何需要限流的场景。 26 | 27 | ## 项目说明 28 | | 项目 | 说明 | 29 | | ---------------------------------------- | ------------------------------------------------------ | 30 | | FireflySoft.RateLmit.Core | 算法、规则等限流核心控制程序。 | 31 | | FireflySoft.RateLimit.AspNet | ASP.NET 限流处理器,支持 .NET 4.6.1 及以上版本。 | 32 | | FireflySoft.RateLimit.AspNetCore | ASP.NET Core 限流中间件,支持 .NET Core 2.0 及后续版本。 | 33 | | FireflySoft.RateLimit.Core.UnitTest | FireflySoft.RateLimit.Core 的单元测试。 | 34 | | FireflySoft.RateLimit.Core.BenchmarkTest | FireflySoft.RateLimit.Core 的基准测试。 | 35 | | Samples/Console | 使用 FireflySoft.RateLmit.Core 的控制台示例程序. | 36 | | Samples/AspNet | 使用 FireflySoft.RateLimit.AspNet 的普通示例程序。 | 37 | | Samples/AspNetCore | 使用 FireflySoft.RateLimit.AspNetCore 的普通示例程序。 | 38 | | Samples/RuleAutoUpdate | 使用 FireflySoft.RateLimit.AspNetCore 的自动更新限流规则的示例程序。 | 39 | 40 | ## 使用说明 41 | 42 | ### ASP.NET Core 应用 43 | 44 | ***1、安装 Nuget 包*** 45 | 46 | 使用包管理器控制台: 47 | 48 | ```shell 49 | Install-Package FireflySoft.RateLimit.AspNetCore 50 | ``` 51 | 52 | 或者使用 .NET CLI: 53 | 54 | ```shell 55 | dotnet add package FireflySoft.RateLimit.AspNetCore 56 | ``` 57 | 58 | 或者直接添加到项目文件中: 59 | ```xml 60 | 61 | 62 | 63 | ``` 64 | 65 | ***2、使用中间件*** 66 | 67 | 在Startup.cs中注册服务并使用[中间件](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1): 68 | 69 | ```csharp 70 | public void ConfigureServices(IServiceCollection services) 71 | { 72 | ... 73 | 74 | services.AddRateLimit(new InProcessFixedWindowAlgorithm( 75 | new[] { 76 | new FixedWindowRule() 77 | { 78 | ExtractTarget = context => 79 | { 80 | // 提取限流目标 81 | // 这里是直接从请求中提取Path作为限流目标,还可以多种组合,甚至去远程查询一些数据 82 | return (context as HttpContext).Request.Path.Value; 83 | }, 84 | CheckRuleMatching = context => 85 | { 86 | // 检查当前请求是否要做限流 87 | // 比如有些Url是不做限流的、有些用户是不做限流的 88 | return true; 89 | }, 90 | Name="default limit rule", 91 | LimitNumber=30, // 限流时间窗口内的最大允许请求数量 92 | StatWindow=TimeSpan.FromSeconds(1) // 限流计数的时间窗口 93 | } 94 | }) 95 | ); 96 | 97 | ... 98 | } 99 | 100 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 101 | { 102 | ... 103 | 104 | app.UseRateLimit(); 105 | 106 | ... 107 | } 108 | ``` 109 | 110 | ### ASP.NET 应用 111 | 112 | ***1、安装 Nuget 包*** 113 | 114 | 使用包管理器控制台: 115 | 116 | ```shell 117 | Install-Package FireflySoft.RateLimit.AspNet 118 | ``` 119 | 120 | ***2、注册消息处理器*** 121 | 122 | 打开 Global.asax.cs,使用下面的代码添加限流处理器: 123 | 124 | ```csharp 125 | protected void Application_Start() 126 | { 127 | ... 128 | 129 | GlobalConfiguration.Configuration.MessageHandlers.Add( 130 | new RateLimitHandler( 131 | new Core.InProcessAlgorithm.InProcessFixedWindowAlgorithm( 132 | new[] { 133 | new FixedWindowRule() 134 | { 135 | ExtractTarget = context => 136 | { 137 | return (context as HttpRequestMessage).RequestUri.AbsolutePath; 138 | }, 139 | CheckRuleMatching = context => 140 | { 141 | return true; 142 | }, 143 | Name="default limit rule", 144 | LimitNumber=30, 145 | StatWindow=TimeSpan.FromSeconds(1) 146 | } 147 | }) 148 | )); 149 | 150 | ... 151 | } 152 | ``` 153 | 154 | ### 其它类型应用 155 | 156 | ***1、安装 Nuget 包*** 157 | 158 | 使用包管理器控制台: 159 | 160 | ```shell 161 | Install-Package FireflySoft.RateLimit.Core 162 | ``` 163 | 164 | 或者 .NET CLI: 165 | 166 | ```shell 167 | dotnet add package FireflySoft.RateLimit.Core 168 | ``` 169 | 170 | ***2、使用限流算法*** 171 | 172 | 使用 *IAlgorithm* 过滤每个请求, 处理 *Check* 方法的返回值。 173 | 174 | ```csharp 175 | // 定义限流规则 176 | var fixedWindowRules = new FixedWindowRule[] 177 | { 178 | new FixedWindowRule() 179 | { 180 | Id = "3", 181 | StatWindow=TimeSpan.FromSeconds(1), 182 | LimitNumber=30, 183 | ExtractTarget = (request) => 184 | { 185 | return (request as SimulationRequest).RequestResource; 186 | }, 187 | CheckRuleMatching = (request) => 188 | { 189 | return true; 190 | }, 191 | } 192 | }; 193 | 194 | // 使用限流算法 195 | IAlgorithm algorithm = new InProcessFixedWindowAlgorithm(fixedWindowRules); 196 | 197 | // 过滤请求 198 | var result = algorithm.Check(new SimulationRequest() 199 | { 200 | RequestId = Guid.NewGuid().ToString(), 201 | RequestResource = "home", 202 | Parameters = new Dictionary() { 203 | { "from","sample" }, 204 | } 205 | }); 206 | ``` 207 | 208 | SimulationRequest是一个自定义请求,你可以把它修改为任何适合自己的请求类型。 -------------------------------------------------------------------------------- /FireflySoft.RateLimit.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.30804.86 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FireflySoft.RateLimit.Core", "FireflySoft.RateLimit.Core\FireflySoft.RateLimit.Core.csproj", "{EC3C2079-1284-49F1-8F38-67B5A71B0483}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Console", "samples\console\console.csproj", "{27FF49FD-09AD-4ABE-B8B9-F387034D3AF6}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FireflySoft.RateLimit.AspNetCore", "FireflySoft.RateLimit.AspNetCore\FireflySoft.RateLimit.AspNetCore.csproj", "{265730D4-995C-4228-9BD7-AE902A82284D}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore", "samples\aspnetcore\aspnetcore.csproj", "{4DDD46C7-81AF-443E-BEB1-2DBF0F467229}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FireflySoft.RateLimit.AspNet", "FireflySoft.RateLimit.AspNet\FireflySoft.RateLimit.AspNet.csproj", "{9CBEAEB7-8BE1-4824-9F0F-98B206B3A549}" 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNet", "samples\aspnet\aspnet.csproj", "{3D7CF23D-24FE-4526-9159-2301EBB81170}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FireflySoft.RateLimit.Core.Test", "FireflySoft.RateLimit.Core.Test\FireflySoft.RateLimit.Core.Test.csproj", "{977FB204-41B8-41AE-85A6-C2F865C98415}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FireflySoft.RateLmit.Core.BenchmarkTest", "FireflySoft.RateLmit.Core.BenchmarkTest\FireflySoft.RateLmit.Core.BenchmarkTest.csproj", "{0E94CBB5-31CF-46CD-B57F-D7D06741657A}" 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{7854209F-39A0-4CA6-88AF-7A3721902060}" 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RuleAutoUpdate", "samples\RuleAutoUpdate\RuleAutoUpdate.csproj", "{72AFDDE1-809F-4A08-A810-0509A0C4BE74}" 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPOrClientId", "Samples\IPOrClientId\IPOrClientId.csproj", "{33676BCA-452A-49FE-8FB4-8CFB0D2ACBAC}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {EC3C2079-1284-49F1-8F38-67B5A71B0483}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {EC3C2079-1284-49F1-8F38-67B5A71B0483}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {EC3C2079-1284-49F1-8F38-67B5A71B0483}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {EC3C2079-1284-49F1-8F38-67B5A71B0483}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {27FF49FD-09AD-4ABE-B8B9-F387034D3AF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {27FF49FD-09AD-4ABE-B8B9-F387034D3AF6}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {27FF49FD-09AD-4ABE-B8B9-F387034D3AF6}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {27FF49FD-09AD-4ABE-B8B9-F387034D3AF6}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {265730D4-995C-4228-9BD7-AE902A82284D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {265730D4-995C-4228-9BD7-AE902A82284D}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {265730D4-995C-4228-9BD7-AE902A82284D}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {265730D4-995C-4228-9BD7-AE902A82284D}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {4DDD46C7-81AF-443E-BEB1-2DBF0F467229}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {4DDD46C7-81AF-443E-BEB1-2DBF0F467229}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {4DDD46C7-81AF-443E-BEB1-2DBF0F467229}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {4DDD46C7-81AF-443E-BEB1-2DBF0F467229}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {9CBEAEB7-8BE1-4824-9F0F-98B206B3A549}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {9CBEAEB7-8BE1-4824-9F0F-98B206B3A549}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {9CBEAEB7-8BE1-4824-9F0F-98B206B3A549}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {9CBEAEB7-8BE1-4824-9F0F-98B206B3A549}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {3D7CF23D-24FE-4526-9159-2301EBB81170}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {3D7CF23D-24FE-4526-9159-2301EBB81170}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {3D7CF23D-24FE-4526-9159-2301EBB81170}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {3D7CF23D-24FE-4526-9159-2301EBB81170}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {977FB204-41B8-41AE-85A6-C2F865C98415}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {977FB204-41B8-41AE-85A6-C2F865C98415}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {977FB204-41B8-41AE-85A6-C2F865C98415}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {977FB204-41B8-41AE-85A6-C2F865C98415}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {0E94CBB5-31CF-46CD-B57F-D7D06741657A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {0E94CBB5-31CF-46CD-B57F-D7D06741657A}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {0E94CBB5-31CF-46CD-B57F-D7D06741657A}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {0E94CBB5-31CF-46CD-B57F-D7D06741657A}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {72AFDDE1-809F-4A08-A810-0509A0C4BE74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {72AFDDE1-809F-4A08-A810-0509A0C4BE74}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {72AFDDE1-809F-4A08-A810-0509A0C4BE74}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {72AFDDE1-809F-4A08-A810-0509A0C4BE74}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {33676BCA-452A-49FE-8FB4-8CFB0D2ACBAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {33676BCA-452A-49FE-8FB4-8CFB0D2ACBAC}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {33676BCA-452A-49FE-8FB4-8CFB0D2ACBAC}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {33676BCA-452A-49FE-8FB4-8CFB0D2ACBAC}.Release|Any CPU.Build.0 = Release|Any CPU 73 | EndGlobalSection 74 | GlobalSection(SolutionProperties) = preSolution 75 | HideSolutionNode = FALSE 76 | EndGlobalSection 77 | GlobalSection(ExtensibilityGlobals) = postSolution 78 | SolutionGuid = {FC338DEA-C8F5-4E27-B00C-3DF079B86A9D} 79 | EndGlobalSection 80 | GlobalSection(NestedProjects) = preSolution 81 | {27FF49FD-09AD-4ABE-B8B9-F387034D3AF6} = {7854209F-39A0-4CA6-88AF-7A3721902060} 82 | {3D7CF23D-24FE-4526-9159-2301EBB81170} = {7854209F-39A0-4CA6-88AF-7A3721902060} 83 | {4DDD46C7-81AF-443E-BEB1-2DBF0F467229} = {7854209F-39A0-4CA6-88AF-7A3721902060} 84 | {72AFDDE1-809F-4A08-A810-0509A0C4BE74} = {7854209F-39A0-4CA6-88AF-7A3721902060} 85 | {33676BCA-452A-49FE-8FB4-8CFB0D2ACBAC} = {7854209F-39A0-4CA6-88AF-7A3721902060} 86 | EndGlobalSection 87 | EndGlobal 88 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/InProcessAlgorithm/CounterDictionary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using FireflySoft.RateLimit.Core.Time; 9 | 10 | namespace FireflySoft.RateLimit.Core.InProcessAlgorithm 11 | { 12 | /// 13 | /// Define a dictionary for rate limiting counter. 14 | /// 15 | /// 16 | public class CounterDictionary 17 | { 18 | private DateTimeOffset _lastExpirationScan; 19 | 20 | private TimeSpan _expirationScanFrequency; 21 | 22 | private ITimeProvider _timeProvider; 23 | 24 | private ConcurrentDictionary> _items = new ConcurrentDictionary>(); 25 | 26 | private ICollection>> _itemsCollection => _items; 27 | 28 | /// 29 | /// Create a new instance 30 | /// 31 | /// 32 | public CounterDictionary(ITimeProvider timeProvider) 33 | { 34 | _timeProvider = timeProvider; 35 | _lastExpirationScan = _timeProvider.GetCurrentLocalTime(); 36 | _expirationScanFrequency = TimeSpan.FromSeconds(60); 37 | } 38 | 39 | /// 40 | /// Gets the count of this dictionary 41 | /// 42 | /// 43 | public int GetCount() 44 | { 45 | var count = _items.Count; 46 | StartScanForExpiredItemsIfNeeded(_timeProvider.GetCurrentLocalTime()); 47 | return count; 48 | } 49 | 50 | /// 51 | /// Set a counter 52 | /// 53 | /// 54 | /// 55 | public void Set(string key, CounterDictionaryItem item) 56 | { 57 | if (item == null) 58 | { 59 | throw new ArgumentNullException("item can not be null."); 60 | } 61 | 62 | var now = _timeProvider.GetCurrentLocalTime(); 63 | 64 | if (item.ExpireTime <= now) 65 | { 66 | throw new InvalidDataException("ExpireTime must great than current time."); 67 | } 68 | 69 | if (_items.TryGetValue(key, out var oldItem)) 70 | { 71 | var updateResult = _items.TryUpdate(key, item, oldItem); 72 | 73 | // maybe removed by scan for expired items, so try add it 74 | if (!updateResult) 75 | { 76 | _items.TryAdd(key, item); 77 | } 78 | 79 | StartScanForExpiredItemsIfNeeded(now); 80 | return; 81 | } 82 | 83 | // For each target, only one thread is accessing, so TryAdd should succeed 84 | if (_items.TryAdd(key, item)) 85 | { 86 | StartScanForExpiredItemsIfNeeded(now); 87 | } 88 | } 89 | 90 | /// 91 | /// Get a counter item 92 | /// 93 | /// 94 | /// 95 | /// 96 | public bool TryGet(string key, out CounterDictionaryItem item) 97 | { 98 | var now = _timeProvider.GetCurrentLocalTime(); 99 | 100 | if (_items.TryGetValue(key, out item)) 101 | { 102 | if (item.IsExpired) 103 | { 104 | StartScanForExpiredItemsIfNeeded(now); 105 | return false; 106 | } 107 | 108 | if (now >= item.ExpireTime) 109 | { 110 | item.IsExpired = true; 111 | StartScanForExpiredItemsIfNeeded(now); 112 | return false; 113 | } 114 | 115 | StartScanForExpiredItemsIfNeeded(now); 116 | return true; 117 | } 118 | 119 | StartScanForExpiredItemsIfNeeded(now); 120 | return false; 121 | } 122 | 123 | // reference: https://github.com/dotnet/runtime/blob/1466e404dfac7ad6af7e6877d26885ce42414120/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs#L327 124 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 125 | private void StartScanForExpiredItemsIfNeeded(DateTimeOffset now) 126 | { 127 | if (_expirationScanFrequency < now - _lastExpirationScan) 128 | { 129 | ScheduleTask(now); 130 | } 131 | 132 | void ScheduleTask(DateTimeOffset dt) 133 | { 134 | _lastExpirationScan = dt; 135 | Task.Factory.StartNew(state => 136 | { 137 | if (state != null) 138 | { 139 | ((CounterDictionary)state).ScanForExpiredItems(); 140 | } 141 | }, this, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); 142 | } 143 | } 144 | 145 | private void ScanForExpiredItems() 146 | { 147 | DateTimeOffset now = _lastExpirationScan = _timeProvider.GetCurrentLocalTime(); 148 | 149 | foreach (KeyValuePair> entry in _items) 150 | { 151 | var item = entry.Value; 152 | 153 | if (item.CheckExpired(now)) 154 | { 155 | _itemsCollection.Remove(new KeyValuePair>(item.Key, item)); 156 | } 157 | } 158 | } 159 | } 160 | 161 | /// 162 | /// Define the item in the CounterDictionary 163 | /// 164 | public class CounterDictionaryItem 165 | { 166 | /// 167 | /// Create a new instance 168 | /// 169 | /// 170 | /// 171 | public CounterDictionaryItem(string key, T counter) 172 | { 173 | Key = key; 174 | Counter = counter; 175 | } 176 | 177 | internal bool IsExpired { get; set; } 178 | 179 | /// 180 | /// The key 181 | /// 182 | /// 183 | public string Key { get; set; } 184 | 185 | /// 186 | /// The counter 187 | /// 188 | /// 189 | public T Counter { get; set; } 190 | 191 | /// 192 | /// The expire time of this item 193 | /// 194 | /// 195 | public DateTimeOffset ExpireTime { get; set; } 196 | 197 | internal bool CheckExpired(DateTimeOffset now) 198 | { 199 | return IsExpired || now > ExpireTime; 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FireflySoft.RateLimit           [中文](https://github.com/bosima/FireflySoft.RateLimit/blob/master/README.zh-CN.md) 2 | 3 | - [FireflySoft.RateLimit           中文](#fireflysoftratelimit中文) 4 | - [Introduction](#introduction) 5 | - [Features](#features) 6 | - [Projects](#projects) 7 | - [Usage](#usage) 8 | - [ASP.NET Core](#aspnet-core) 9 | - [ASP.NET](#aspnet) 10 | - [Others](#others) 11 | 12 | ## Introduction 13 | Fireflysoft.RateLimit is a rate limiting library based on .Net standard. Its core is simple and lightweight, and can flexibly meet the rate limiting needs of many scenarios. 14 | 15 | ## Features 16 | * Multiple rate limiting algorithms: built-in fixed window, sliding window, leaky bucket, token bucket, and can be extended. 17 | * Multiple counting storage: memory and Redis (including cluster). 18 | * Distributed friendly: supports unified counting of distributed programs with Redis storage. 19 | * Flexible rate limiting targets: each data can be extracted from the request to set rate limiting targets. 20 | * Support rate limit penalty: the client can be locked for a period of time after the rate limit is triggered. 21 | * Time window enhancement: support to the millisecond level; support starting from the starting point of time periods such as seconds, minutes, hours, dates, etc. 22 | * Real-time tracking: the number of requests processed and the remaining allowed requests in the current counting cycle, as well as the reset time of the counting cycle. 23 | * Dynamically change the rules: support the dynamic change of the rate limiting rules when the program is running. 24 | * Custom error: you can customize the error code and error message after the current limit is triggered. 25 | * Universality: in principle, it can meet any scenario that requires rate limiting. 26 | 27 | ## Projects 28 | | Project | Descriptioin | 29 | | ---------------------------------------- | ------------------------------------------------------ | 30 | | FireflySoft.RateLmit.Core | algorithm, rules, persistence and other core codes. | 31 | | FireflySoft.RateLimit.AspNet | ASP.NET rate-limit middleware based on .NET Framework. | 32 | | FireflySoft.RateLimit.AspNetCore | ASP.NET Core rate-limit middleware. | 33 | | FireflySoft.RateLimit.Core.UnitTest | Unit test for FireflySoft.RateLimit.Core. | 34 | | FireflySoft.RateLimit.Core.BenchmarkTest | Benchmark test for FireflySoft.RateLimit.Core. | 35 | | Samples/Console | FireflySoft.RateLmit.Core sample program. | 36 | | Samples/AspNet | FireflySoft.RateLimit.AspNet sample program. | 37 | | Samples/AspNetCore | FireflySoft.RateLimit.AspNetCore sample program. | 38 | | Samples/RuleAutoUpdate | A sample that can automatic update rate limiting rules. | 39 | ## Usage 40 | 41 | ### ASP.NET Core 42 | 43 | ***1、Install Nuget Package*** 44 | 45 | Package Manager: 46 | 47 | ```shell 48 | Install-Package FireflySoft.RateLimit.AspNetCore 49 | ``` 50 | 51 | Or .NET CLI: 52 | 53 | ```shell 54 | dotnet add package FireflySoft.RateLimit.AspNetCore 55 | ``` 56 | 57 | Or Project file: 58 | ```xml 59 | 60 | 61 | 62 | ``` 63 | 64 | ***2、Use Middleware*** 65 | 66 | The following code calls the rate-limit [middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1) from Startup.Configure: 67 | 68 | ```csharp 69 | public void ConfigureServices(IServiceCollection services) 70 | { 71 | ... 72 | 73 | services.AddRateLimit(new InProcessFixedWindowAlgorithm( 74 | new[] { 75 | new FixedWindowRule() 76 | { 77 | ExtractTarget = context => 78 | { 79 | return (context as HttpContext).Request.Path.Value; 80 | }, 81 | CheckRuleMatching = context => 82 | { 83 | return true; 84 | }, 85 | Name="default limit rule", 86 | LimitNumber=30, 87 | StatWindow=TimeSpan.FromSeconds(1) 88 | } 89 | }) 90 | ); 91 | 92 | ... 93 | } 94 | 95 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 96 | { 97 | ... 98 | 99 | app.UseRateLimit(); 100 | 101 | ... 102 | } 103 | ``` 104 | 105 | ### ASP.NET 106 | 107 | ***1、Install Nuget Package:*** 108 | 109 | Package Manager: 110 | 111 | ```shell 112 | Install-Package FireflySoft.RateLimit.AspNet 113 | ``` 114 | 115 | ***2、Register MessageHandler*** 116 | 117 | Open Global.asax.cs, the following code adds the rate limit message handle: 118 | 119 | ```csharp 120 | protected void Application_Start() 121 | { 122 | ... 123 | 124 | GlobalConfiguration.Configuration.MessageHandlers.Add( 125 | new RateLimitHandler( 126 | new Core.InProcessAlgorithm.InProcessFixedWindowAlgorithm( 127 | new[] { 128 | new FixedWindowRule() 129 | { 130 | ExtractTarget = context => 131 | { 132 | return (context as HttpRequestMessage).RequestUri.AbsolutePath; 133 | }, 134 | CheckRuleMatching = context => 135 | { 136 | return true; 137 | }, 138 | Name="default limit rule", 139 | LimitNumber=30, 140 | StatWindow=TimeSpan.FromSeconds(1) 141 | } 142 | }) 143 | )); 144 | 145 | ... 146 | } 147 | ``` 148 | 149 | ### Others 150 | 151 | ***1、Install Nuget Package*** 152 | 153 | Package Manager: 154 | 155 | ```shell 156 | Install-Package FireflySoft.RateLimit.Core 157 | ``` 158 | 159 | Or .NET CLI: 160 | 161 | ```shell 162 | dotnet add package FireflySoft.RateLimit.Core 163 | ``` 164 | 165 | ***2、Use IAlgorithm*** 166 | 167 | Use *IAlgorithm* to filter every request, process the return value of *Check* method. 168 | 169 | ```csharp 170 | // Rule 171 | var fixedWindowRules = new FixedWindowRule[] 172 | { 173 | new FixedWindowRule() 174 | { 175 | Id = "3", 176 | StatWindow=TimeSpan.FromSeconds(1), 177 | LimitNumber=30, 178 | ExtractTarget = (request) => 179 | { 180 | return (request as SimulationRequest).RequestResource; 181 | }, 182 | CheckRuleMatching = (request) => 183 | { 184 | return true; 185 | }, 186 | } 187 | }; 188 | 189 | // Algorithm 190 | IAlgorithm algorithm = new InProcessFixedWindowAlgorithm(fixedWindowRules); 191 | 192 | // Check 193 | var result = algorithm.Check(new SimulationRequest() 194 | { 195 | RequestId = Guid.NewGuid().ToString(), 196 | RequestResource = "home", 197 | Parameters = new Dictionary() { 198 | { "from","sample" }, 199 | } 200 | }); 201 | ``` 202 | 203 | SimulationRequest is a custom request that you can modify to any type. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/InProcessAlgorithm/InProcessTokenBucketAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FireflySoft.RateLimit.Core.Rule; 5 | using FireflySoft.RateLimit.Core.Time; 6 | 7 | namespace FireflySoft.RateLimit.Core.InProcessAlgorithm 8 | { 9 | /// 10 | /// Define an in-process token bucket algorithm 11 | /// 12 | public class InProcessTokenBucketAlgorithm : BaseInProcessAlgorithm 13 | { 14 | readonly CounterDictionary _tokenBuckets; 15 | 16 | /// 17 | /// Create a new instance 18 | /// 19 | /// The rate limit rules 20 | /// The time provider 21 | /// If rules can be updated 22 | public InProcessTokenBucketAlgorithm(IEnumerable rules, ITimeProvider timeProvider = null, bool updatable = false) 23 | : base(rules, timeProvider, updatable) 24 | { 25 | _tokenBuckets = new CounterDictionary(_timeProvider); 26 | } 27 | 28 | /// 29 | /// check single rule for target 30 | /// 31 | /// 32 | /// 33 | /// 34 | protected override RuleCheckResult CheckSingleRule(string target, RateLimitRule rule) 35 | { 36 | var currentRule = rule as TokenBucketRule; 37 | var amount = 1; 38 | 39 | var result = InnerCheckSingleRule(target, amount, currentRule); 40 | return new RuleCheckResult() 41 | { 42 | IsLimit = result.IsLimit, 43 | Target = target, 44 | Count = currentRule.GetLimitThreshold() - result.Remaining, 45 | Remaining = result.Remaining, 46 | Rule = rule, 47 | ResetTime = result.ResetTime, 48 | }; 49 | } 50 | 51 | /// 52 | /// check single rule for target 53 | /// 54 | /// 55 | /// 56 | /// 57 | protected override async Task CheckSingleRuleAsync(string target, RateLimitRule rule) 58 | { 59 | return await Task.FromResult(CheckSingleRule(target, rule)).ConfigureAwait(false); 60 | } 61 | 62 | /// 63 | /// Decrease the count value of the rate limit target for token bucket algorithm. 64 | /// 65 | /// The target 66 | /// The amount of decrease 67 | /// The rate limit rule 68 | /// Amount of token in the bucket 69 | public (bool IsLimit, long Remaining, DateTimeOffset ResetTime) InnerCheckSingleRule(string target, long amount, TokenBucketRule currentRule) 70 | { 71 | bool locked = CheckLocked(target, out DateTimeOffset? expireTime); 72 | if (locked) 73 | { 74 | return (true, -1, expireTime.Value); 75 | } 76 | 77 | var currentTime = _timeProvider.GetCurrentLocalTime(); 78 | (bool IsLimit, long Remaining, DateTimeOffset ExpireTime) countResult; 79 | 80 | lock (target) 81 | { 82 | countResult = Count(target, amount, currentRule, currentTime); 83 | } 84 | 85 | // do free lock 86 | var checkResult = countResult.Item1; 87 | if (checkResult) 88 | { 89 | if (currentRule.LockSeconds > 0) 90 | { 91 | expireTime = currentTime.AddSeconds(currentRule.LockSeconds); 92 | TryLock(target, expireTime.Value); 93 | return (true, countResult.Remaining, expireTime.Value); 94 | } 95 | } 96 | 97 | return countResult; 98 | } 99 | 100 | private (bool IsLimit, long Remaining, DateTimeOffset ExpireTime) Count(string target, long amount, TokenBucketRule currentRule, DateTimeOffset currentTime) 101 | { 102 | long bucketAmount = 0; 103 | 104 | if (!_tokenBuckets.TryGet(target, out var cacheItem)) 105 | { 106 | // In the initial state, the bucket is full 107 | bucketAmount = currentRule.Capacity - amount; 108 | cacheItem = AddNewBucket(target, bucketAmount, currentRule, currentTime); 109 | return (false, bucketAmount, cacheItem.Counter.LastInflowTime.Add(currentRule.InflowUnit)); 110 | } 111 | 112 | var counter = (TokenBucketCounter)cacheItem.Counter; 113 | 114 | // If the capacity is reduced to less than the number of remaining tokens, 115 | // the tokens that cannot be placed in the bucket are removed. 116 | // But after the capacity increases, the number of tokens in the bucket will not increase directly, 117 | // which will gradually increase with the inflow. 118 | if (currentRule.Capacity < counter.Value) 119 | { 120 | counter.Value = currentRule.Capacity; 121 | } 122 | 123 | var inflowUnitMilliseconds = currentRule.InflowUnit.TotalMilliseconds; 124 | var lastInflowTime = counter.LastInflowTime; 125 | var pastMilliseconds = (currentTime - lastInflowTime).TotalMilliseconds; 126 | if (pastMilliseconds < inflowUnitMilliseconds) 127 | { 128 | // In the same time window as the previous request, only the token is taken from the bucket 129 | bucketAmount = counter.Value - amount; 130 | } 131 | else 132 | { 133 | // After one or more time windows, some tokens need to be put into the bucket, 134 | // and the number of tokens in the bucket needs to be recalculated. 135 | var pastInflowUnits = (int)(pastMilliseconds / inflowUnitMilliseconds); 136 | lastInflowTime = lastInflowTime.AddMilliseconds(pastInflowUnits * inflowUnitMilliseconds); 137 | var pastInflowQuantity = currentRule.InflowQuantityPerUnit * pastInflowUnits; 138 | bucketAmount = (counter.Value < 0 ? 0 : counter.Value) + pastInflowQuantity - amount; 139 | 140 | counter.LastInflowTime = lastInflowTime; 141 | cacheItem.ExpireTime = lastInflowTime.Add(currentRule.MinFillTime); 142 | } 143 | 144 | // Trigger rate limiting 145 | if (bucketAmount < 0) 146 | { 147 | return (true, 0, cacheItem.Counter.LastInflowTime.Add(currentRule.InflowUnit)); 148 | } 149 | 150 | // Token bucket full 151 | if (bucketAmount >= currentRule.Capacity) 152 | { 153 | bucketAmount = currentRule.Capacity - amount; 154 | } 155 | counter.Value = bucketAmount; 156 | 157 | return (false, counter.Value, cacheItem.Counter.LastInflowTime.Add(currentRule.InflowUnit)); 158 | } 159 | 160 | private CounterDictionaryItem AddNewBucket(string target, long amount, TokenBucketRule currentRule, DateTimeOffset currentTime) 161 | { 162 | var startTime = AlgorithmStartTime.ToSpecifiedTypeTime(currentTime, currentRule.InflowUnit, currentRule.StartTimeType); 163 | var counter = new TokenBucketCounter() 164 | { 165 | Value = amount, 166 | LastInflowTime = startTime, 167 | }; 168 | var cacheItem = new CounterDictionaryItem(target, counter) 169 | { 170 | ExpireTime = startTime.Add(currentRule.MinFillTime) 171 | }; 172 | _tokenBuckets.Set(target, cacheItem); 173 | return cacheItem; 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/RedisAlgorithm/RedisTokenBucketAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FireflySoft.RateLimit.Core.Rule; 5 | using FireflySoft.RateLimit.Core.Time; 6 | using StackExchange.Redis; 7 | 8 | namespace FireflySoft.RateLimit.Core.RedisAlgorithm 9 | { 10 | /// 11 | /// Redis Token Bucket Algorithm 12 | /// 13 | public class RedisTokenBucketAlgorithm : BaseRedisAlgorithm 14 | { 15 | private readonly RedisLuaScript _tokenBucketDecrementLuaScript; 16 | 17 | /// 18 | /// Create a new instance 19 | /// 20 | /// The rate limit rules 21 | /// The redis client 22 | /// The provider of current time 23 | /// If rules can be updated 24 | public RedisTokenBucketAlgorithm(IEnumerable rules, ConnectionMultiplexer redisClient = null, ITimeProvider timeProvider = null, bool updatable = false) 25 | : base(rules, redisClient, timeProvider, updatable) 26 | { 27 | _tokenBucketDecrementLuaScript = new RedisLuaScript(_redisClient, "Src-DecrWithTokenBucket", 28 | @"local ret={} 29 | local current_time=tonumber(ARGV[5]) 30 | local cl_key = '{' .. KEYS[1] .. '}' 31 | local lock_key = cl_key .. '-lock' 32 | local lock_val = redis.call('get',lock_key) 33 | if lock_val == '1' then 34 | ret[1]=1 35 | ret[2]=-1 36 | local lock_ttl=redis.call('PTTL',lock_key) 37 | ret[3]=tonumber(lock_ttl)+current_time 38 | return ret; 39 | end 40 | ret[1]=0 41 | local st_key= cl_key .. '-st' 42 | local amount=tonumber(ARGV[1]) 43 | local capacity=tonumber(ARGV[2]) 44 | local inflow_unit=tonumber(ARGV[3]) 45 | local inflow_quantity_per_unit=tonumber(ARGV[4]) 46 | local start_time=tonumber(ARGV[6]) 47 | local lock_seconds=tonumber(ARGV[7]) 48 | local st_expire_ms=math.ceil((capacity/inflow_quantity_per_unit)*inflow_unit)*2 49 | local val_expire_ms=st_expire_ms+10 50 | local bucket_amount=0 51 | local last_time=redis.call('get',st_key) 52 | if(last_time==false) 53 | then 54 | bucket_amount = capacity - amount; 55 | redis.call('set',KEYS[1],bucket_amount,'PX',val_expire_ms) 56 | redis.call('set',st_key,start_time,'PX',st_expire_ms) 57 | ret[2]=bucket_amount 58 | ret[3]=start_time+inflow_unit 59 | return ret 60 | end 61 | 62 | local current_value = redis.call('get',KEYS[1]) 63 | current_value = tonumber(current_value) 64 | if (capacity < current_value) then 65 | current_value = capacity 66 | end 67 | last_time=tonumber(last_time) 68 | local last_time_changed=0 69 | local past_time=current_time-last_time 70 | if(past_time=capacity) 83 | then 84 | bucket_amount=capacity-amount 85 | end 86 | ret[2]=bucket_amount 87 | ret[3]=last_time+inflow_unit 88 | if(bucket_amount<0) 89 | then 90 | if lock_seconds>0 then 91 | redis.call('set',lock_key,'1','EX',lock_seconds,'NX') 92 | ret[3]=current_time+lock_seconds*1000 93 | end 94 | ret[1]=1 95 | ret[2]=0 96 | return ret 97 | end 98 | 99 | if last_time_changed==1 then 100 | redis.call('set',KEYS[1],bucket_amount,'PX',val_expire_ms) 101 | redis.call('set',st_key,last_time,'PX',st_expire_ms) 102 | else 103 | redis.call('set',KEYS[1],bucket_amount,'PX',val_expire_ms) 104 | end 105 | return ret"); 106 | } 107 | 108 | /// 109 | /// check single rule for target 110 | /// 111 | /// 112 | /// 113 | /// 114 | protected override RuleCheckResult CheckSingleRule(string target, RateLimitRule rule) 115 | { 116 | var currentRule = rule as TokenBucketRule; 117 | var amount = 1; 118 | 119 | var inflowUnit = currentRule.InflowUnit.TotalMilliseconds; 120 | var currentTime = _timeProvider.GetCurrentUtcMilliseconds(); 121 | var startTime = AlgorithmStartTime.ToSpecifiedTypeTime(currentTime, TimeSpan.FromMilliseconds(inflowUnit), currentRule.StartTimeType); 122 | 123 | var ret = (long[])EvaluateScript(_tokenBucketDecrementLuaScript, new RedisKey[] { target }, 124 | new RedisValue[] { amount, currentRule.Capacity, inflowUnit, currentRule.InflowQuantityPerUnit, currentTime, startTime, currentRule.LockSeconds }); 125 | return new RuleCheckResult() 126 | { 127 | IsLimit = ret[0] == 0 ? false : true, 128 | Target = target, 129 | Count = currentRule.Capacity - ret[1], 130 | Remaining = ret[1], 131 | Rule = rule, 132 | ResetTime = DateTimeOffset.FromUnixTimeMilliseconds(ret[2]).ToLocalTime(), 133 | }; 134 | } 135 | 136 | /// 137 | /// async check single rule for target 138 | /// 139 | /// 140 | /// 141 | /// 142 | protected override async Task CheckSingleRuleAsync(string target, RateLimitRule rule) 143 | { 144 | var currentRule = rule as TokenBucketRule; 145 | var amount = 1; 146 | 147 | var inflowUnit = currentRule.InflowUnit.TotalMilliseconds; 148 | var currentTime = await _timeProvider.GetCurrentUtcMillisecondsAsync().ConfigureAwait(false); 149 | var startTime = AlgorithmStartTime.ToSpecifiedTypeTime(currentTime, TimeSpan.FromMilliseconds(inflowUnit), currentRule.StartTimeType); 150 | 151 | var ret = (long[])await EvaluateScriptAsync(_tokenBucketDecrementLuaScript, new RedisKey[] { target }, 152 | new RedisValue[] { amount, currentRule.Capacity, inflowUnit, currentRule.InflowQuantityPerUnit, currentTime, startTime, currentRule.LockSeconds }) 153 | .ConfigureAwait(false); 154 | var result = new Tuple(ret[0] == 0 ? false : true, ret[1]); 155 | return new RuleCheckResult() 156 | { 157 | IsLimit = ret[0] == 0 ? false : true, 158 | Target = target, 159 | Count = currentRule.Capacity - ret[1], 160 | Remaining = ret[1], 161 | Rule = rule, 162 | ResetTime = DateTimeOffset.FromUnixTimeMilliseconds(ret[2]).ToLocalTime(), 163 | }; 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/InProcessAlgorithm/MemorySlidingWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.CompilerServices; 4 | using FireflySoft.RateLimit.Core.Rule; 5 | using FireflySoft.RateLimit.Core.Time; 6 | 7 | namespace FireflySoft.RateLimit.Core.InProcessAlgorithm 8 | { 9 | /// 10 | /// The sliding window item 11 | /// 12 | public struct SlidingWindowPeriod 13 | { 14 | /// 15 | /// The key 16 | /// 17 | public long Key; 18 | 19 | /// 20 | /// The count value 21 | /// 22 | public long CountValue; 23 | } 24 | 25 | /// 26 | /// Define a sliding window in memory 27 | /// 28 | public class MemorySlidingWindow 29 | { 30 | int _length; 31 | SlidingWindowPeriod[] _queue; 32 | SlidingWindowRule _rule; 33 | long _statPeriodMilliseconds; 34 | long _startPeriod; 35 | int _head = 0; 36 | int _tail = 0; 37 | 38 | /// 39 | /// Create a new instance of MemorySlidingWindow 40 | /// 41 | /// 42 | public MemorySlidingWindow(SlidingWindowRule rule) 43 | { 44 | _length = rule.PeriodNumber; 45 | _queue = new SlidingWindowPeriod[_length]; 46 | _rule = rule; 47 | _statPeriodMilliseconds = (long)rule.StatPeriod.TotalMilliseconds; 48 | } 49 | 50 | /// 51 | /// Reset when rule changed 52 | /// 53 | /// 54 | public void ResetIfRuleChanged(SlidingWindowRule rule) 55 | { 56 | if (rule.StatPeriod.Ticks == _rule.StatPeriod.Ticks && 57 | rule.StatWindow.Ticks == _rule.StatWindow.Ticks) 58 | { 59 | return; 60 | } 61 | 62 | var newLength = rule.PeriodNumber; 63 | var newQueue = new SlidingWindowPeriod[newLength]; 64 | var newTail = 0; 65 | 66 | // Only handle the case where 'StatPeriod' has not changed. 67 | // When StatPeriod changes, simply restart the sliding window. 68 | // Because 'Period' is the minimum count period, 69 | // the value of the smaller count period cannot be accurately calculated, 70 | // so the count value of the new 'Period' cannot be calculated. 71 | if (rule.StatPeriod.Ticks == _rule.StatPeriod.Ticks) 72 | { 73 | var loopIndex = _tail; 74 | newTail = _length - 1; 75 | if (rule.StatWindow.Ticks < _rule.StatWindow.Ticks) 76 | { 77 | newTail = newLength - 1; 78 | } 79 | 80 | for (int i = newTail; i >= 0; i--) 81 | { 82 | newQueue[i] = _queue[loopIndex]; 83 | loopIndex--; 84 | if (loopIndex < 0) 85 | { 86 | loopIndex = _length - 1; 87 | } 88 | } 89 | } 90 | 91 | _length = newQueue.Length; 92 | _queue = newQueue; 93 | _head = 0; 94 | _tail = newTail; 95 | _rule = rule; 96 | } 97 | 98 | /// 99 | /// Increment the 'CountValue' of the specified period 100 | /// 101 | /// The index of specified period in time window 102 | /// 103 | /// The count value of the current period after increment 104 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 105 | public long IncreamentPeriod(int periodIndex, int amount) 106 | { 107 | _queue[periodIndex].CountValue += amount; 108 | return _queue[periodIndex].CountValue; 109 | } 110 | 111 | /// 112 | /// Gets the count value of the sliding window 113 | /// 114 | /// 115 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 116 | public long GetCount() 117 | { 118 | return _queue.Sum(d => d.CountValue); 119 | } 120 | 121 | /// 122 | /// Gets the period index of the current time in the sliding window 123 | /// 124 | /// 125 | public (int periodIndex, long periodId) LoadPeriod(long currentMilliseconds) 126 | { 127 | var currentPeriodResult = GetCurrentPeriod(currentMilliseconds); 128 | var currentPeriod = currentPeriodResult.Item1; 129 | var pastPeriods = currentPeriodResult.Item2; 130 | 131 | var tailPeriod = _queue[_tail]; 132 | 133 | // first use sliding window 134 | if (_tail == _head && tailPeriod.Key == 0) 135 | { 136 | var firstPeriod = new SlidingWindowPeriod() 137 | { 138 | Key = currentPeriod, 139 | CountValue = 0 140 | }; 141 | _queue[_tail] = firstPeriod; 142 | return (_tail, firstPeriod.Key); 143 | } 144 | 145 | // The current period is exactly corresponding to the tail of the queue 146 | if (currentPeriod == tailPeriod.Key) 147 | { 148 | return (_tail, tailPeriod.Key); 149 | } 150 | 151 | // In the case of high concurrency, the previous period may be obtained 152 | // It is simply considered as the previous period 153 | if (currentPeriod < tailPeriod.Key) 154 | { 155 | int index = _tail; 156 | index--; 157 | if (index < 0) index += _length; 158 | return (index, _queue[index].Key); 159 | } 160 | 161 | // if 'currentPeriod' greater than the last period, we need create new period 162 | CreatePastPeriod(pastPeriods, tailPeriod); 163 | return (_tail, _queue[_tail].Key); 164 | } 165 | 166 | private void CreatePastPeriod(int pastPeriods, SlidingWindowPeriod lastPeriod) 167 | { 168 | for (int i = 1; i <= pastPeriods; i++) 169 | { 170 | var newPeriod = new SlidingWindowPeriod() 171 | { 172 | Key = lastPeriod.Key + _statPeriodMilliseconds * i, 173 | CountValue = 0 174 | }; 175 | 176 | _tail++; 177 | if (_tail == _length) _tail = 0; 178 | 179 | // this is a circular queue 180 | if (_tail <= _head) 181 | { 182 | _head++; 183 | if (_head == _length) _head = 0; 184 | } 185 | _queue[_tail] = newPeriod; 186 | } 187 | } 188 | 189 | private Tuple GetCurrentPeriod(long currentMilliseconds) 190 | { 191 | long currentPeriod = 0; 192 | int pastPeriods = 0; 193 | 194 | if (_startPeriod == 0) 195 | { 196 | var startTimeMilliseconds = AlgorithmStartTime.ToSpecifiedTypeTime(currentMilliseconds, _rule.StatWindow, _rule.StartTimeType); 197 | _startPeriod = startTimeMilliseconds + _statPeriodMilliseconds - 1; 198 | currentPeriod = _startPeriod; 199 | } 200 | else 201 | { 202 | var tailPeriod = _queue[_tail].Key; 203 | var pastMilliseconds = currentMilliseconds - tailPeriod; 204 | if (pastMilliseconds <= 0) 205 | { 206 | currentPeriod = tailPeriod; 207 | } 208 | else 209 | { 210 | pastPeriods = (int)Math.Ceiling(pastMilliseconds / (double)_statPeriodMilliseconds); 211 | currentPeriod = tailPeriod + pastPeriods * _statPeriodMilliseconds; 212 | } 213 | } 214 | 215 | return new Tuple(currentPeriod, pastPeriods); 216 | } 217 | } 218 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/InProcessAlgorithm/InProcessLeakyBucketAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FireflySoft.RateLimit.Core.Rule; 5 | using FireflySoft.RateLimit.Core.Time; 6 | 7 | namespace FireflySoft.RateLimit.Core.InProcessAlgorithm 8 | { 9 | /// 10 | /// Define an in-process leaky bucket algorithm 11 | /// 12 | public class InProcessLeakyBucketAlgorithm : BaseInProcessAlgorithm 13 | { 14 | readonly CounterDictionary _leakyBuckets; 15 | 16 | /// 17 | /// Create a new instance 18 | /// 19 | /// The rate limit rules 20 | /// The time provider 21 | /// If rules can be updated 22 | public InProcessLeakyBucketAlgorithm(IEnumerable rules, ITimeProvider timeProvider = null, bool updatable = false) 23 | : base(rules, timeProvider, updatable) 24 | { 25 | _leakyBuckets = new CounterDictionary(_timeProvider); 26 | } 27 | 28 | /// 29 | /// Check single rule for target 30 | /// 31 | /// 32 | /// 33 | /// 34 | protected override RuleCheckResult CheckSingleRule(string target, RateLimitRule rule) 35 | { 36 | var currentRule = rule as LeakyBucketRule; 37 | var amount = 1; 38 | 39 | var result = InnerCheckSingleRule(target, amount, currentRule); 40 | return new RuleCheckResult() 41 | { 42 | IsLimit = result.IsLimit, 43 | Target = target, 44 | Count = result.Count, 45 | Remaining=currentRule.GetLimitThreshold()-result.Count, 46 | Rule = rule, 47 | Wait = result.Wait, 48 | ResetTime = result.ResetTime, 49 | }; 50 | } 51 | 52 | /// 53 | /// Check single rule for target 54 | /// 55 | /// 56 | /// 57 | /// 58 | protected override async Task CheckSingleRuleAsync(string target, RateLimitRule rule) 59 | { 60 | return await Task.FromResult(CheckSingleRule(target, rule)).ConfigureAwait(false); 61 | } 62 | 63 | /// 64 | /// Increase the count value of the rate limit target for leaky bucket algorithm. 65 | /// 66 | /// The target 67 | /// amount of increase 68 | /// The current rule 69 | /// Amount of request in the bucket 70 | public (bool IsLimit, long Count, long Wait, DateTimeOffset ResetTime) InnerCheckSingleRule(string target, long amount, LeakyBucketRule currentRule) 71 | { 72 | bool locked = CheckLocked(target, out DateTimeOffset? expireTime); 73 | if (locked) 74 | { 75 | return (true, -1L, -1L, expireTime.Value); 76 | } 77 | 78 | var currentTime = _timeProvider.GetCurrentLocalTime(); 79 | (bool IsLimit, long Count, long Wait, DateTimeOffset ResetTime) countResult; 80 | 81 | lock (target) 82 | { 83 | countResult = Count(target, amount, currentRule, currentTime); 84 | } 85 | 86 | // do free lock 87 | var checkResult = countResult.IsLimit; 88 | if (checkResult) 89 | { 90 | if (currentRule.LockSeconds > 0) 91 | { 92 | expireTime = currentTime.AddSeconds(currentRule.LockSeconds); 93 | TryLock(target, expireTime.Value); 94 | return (true, countResult.Count, -1L, expireTime.Value); 95 | } 96 | } 97 | 98 | return countResult; 99 | } 100 | 101 | private (bool IsLimit, long Count, long Wait, DateTimeOffset ResetTime) Count(string target, long amount, LeakyBucketRule currentRule, DateTimeOffset currentTime) 102 | { 103 | if (!_leakyBuckets.TryGet(target, out var cacheItem)) 104 | { 105 | cacheItem = AddNewBucket(target, amount, currentRule, currentTime); 106 | return (false, amount, 0L, cacheItem.Counter.LastFlowOutTime.Add(currentRule.OutflowUnit)); 107 | } 108 | 109 | var counter = (LeakyBucketCounter)cacheItem.Counter; 110 | var countValue = counter.Value; 111 | var lastFlowOutTime = counter.LastFlowOutTime; 112 | var pastMilliseconds = (currentTime - lastFlowOutTime).TotalMilliseconds; 113 | var outflowUnitMilliseconds = (int)currentRule.OutflowUnit.TotalMilliseconds; 114 | 115 | // After several time windows, some requests flow out, 116 | // and the number of requests in the leaky bucket needs to be recalculated 117 | if (pastMilliseconds >= outflowUnitMilliseconds) 118 | { 119 | var pastOutflowUnitQuantity = (int)(pastMilliseconds / outflowUnitMilliseconds); 120 | if (countValue < currentRule.OutflowQuantityPerUnit) 121 | { 122 | countValue = 0; 123 | } 124 | else 125 | { 126 | var pastOutflowQuantity = currentRule.OutflowQuantityPerUnit * pastOutflowUnitQuantity; 127 | countValue = countValue - pastOutflowQuantity; 128 | countValue = countValue > 0 ? countValue : 0; 129 | } 130 | 131 | lastFlowOutTime = lastFlowOutTime.AddMilliseconds(pastOutflowUnitQuantity * outflowUnitMilliseconds); 132 | pastMilliseconds = (currentTime - lastFlowOutTime).TotalMilliseconds; 133 | 134 | counter.LastFlowOutTime = lastFlowOutTime; 135 | cacheItem.ExpireTime = lastFlowOutTime.Add(currentRule.MaxDrainTime); 136 | } 137 | 138 | // If the number of requests in the current time window is less than the outflow rate, 139 | // the request passes directly without waiting. 140 | countValue = countValue + amount; 141 | if (countValue <= currentRule.OutflowQuantityPerUnit) 142 | { 143 | counter.Value = countValue; 144 | return (false, countValue, 0L, counter.LastFlowOutTime.Add(currentRule.OutflowUnit)); 145 | } 146 | 147 | // Trigger rate limiting 148 | // No need to update counter.Value 149 | if (countValue > currentRule.LimitNumber) 150 | { 151 | countValue = countValue - amount; 152 | return (true, countValue, -1L, counter.LastFlowOutTime.Add(currentRule.OutflowUnit)); 153 | } 154 | 155 | counter.Value = countValue; 156 | 157 | // The requests in the leaky bucket will be processed after one or more time windows. 158 | long wait = CalculateWaitTime(currentRule.OutflowQuantityPerUnit, outflowUnitMilliseconds, pastMilliseconds, countValue); 159 | return (false, countValue, wait, counter.LastFlowOutTime.Add(currentRule.OutflowUnit)); 160 | } 161 | 162 | private CounterDictionaryItem AddNewBucket(string target, long amount, LeakyBucketRule currentRule, DateTimeOffset currentTime) 163 | { 164 | var startTime = AlgorithmStartTime.ToSpecifiedTypeTime(currentTime, currentRule.OutflowUnit, currentRule.StartTimeType); 165 | var counter = new LeakyBucketCounter() 166 | { 167 | Value = amount, 168 | LastFlowOutTime = startTime, 169 | }; 170 | DateTimeOffset expireTime = startTime.Add(currentRule.MaxDrainTime); 171 | var cacheItem = new CounterDictionaryItem(target, counter) 172 | { 173 | ExpireTime = expireTime 174 | }; 175 | _leakyBuckets.Set(target, cacheItem); 176 | return cacheItem; 177 | } 178 | 179 | private static long CalculateWaitTime(long outflowQuantityPerUnit, long outflowUnit, double pastTimeMilliseconds, long countValue) 180 | { 181 | long wait = 0; 182 | 183 | var batchNumber = (int)Math.Ceiling(countValue / (double)outflowQuantityPerUnit) - 1; 184 | if (batchNumber == 1) 185 | { 186 | wait = (long)(outflowUnit - pastTimeMilliseconds); 187 | } 188 | else 189 | { 190 | wait = (long)(outflowUnit * (batchNumber - 1) + (outflowUnit - pastTimeMilliseconds)); 191 | } 192 | 193 | return wait; 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /FireflySoft.RateLimit.Core/RedisAlgorithm/RedisSlidingWindowAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using FireflySoft.RateLimit.Core.Rule; 6 | using FireflySoft.RateLimit.Core.Time; 7 | using StackExchange.Redis; 8 | 9 | namespace FireflySoft.RateLimit.Core.RedisAlgorithm 10 | { 11 | /// 12 | /// 13 | /// 14 | public class RedisSlidingWindowAlgorithm : BaseRedisAlgorithm 15 | { 16 | private readonly RedisLuaScript _slidingWindowIncrementLuaScript; 17 | 18 | /// 19 | /// Create a new instance 20 | /// 21 | /// The rate limit rules 22 | /// The redis client 23 | /// The provider of current time 24 | /// If rules can be updated 25 | public RedisSlidingWindowAlgorithm(IEnumerable rules, ConnectionMultiplexer redisClient = null, ITimeProvider timeProvider = null, bool updatable = false) 26 | : base(rules, redisClient, timeProvider, updatable) 27 | { 28 | // Processing logic for changing the rate limiting rule: 29 | // If only the StatWindow is changed, the Period KV that has been created continues to be valid, 30 | // so the program does not need to do anything. 31 | // If StatPeriod changes, 32 | // the already generated Period is valid only if its value is a multiple or submultiple of the original value, 33 | // otherwise the sliding window is restarted. 34 | _slidingWindowIncrementLuaScript = new RedisLuaScript(_redisClient, "Src-IncrWithExpireSec", 35 | @"local ret={} 36 | local current_time=tonumber(ARGV[5]) 37 | local cl_key='{' .. KEYS[1] .. '}' 38 | local lock_key=cl_key .. '-lock' 39 | local lock_val=redis.call('get',lock_key) 40 | if lock_val == '1' then 41 | ret[1]=1 42 | ret[2]=-1 43 | local lock_ttl=redis.call('PTTL',lock_key) 44 | ret[3]=tonumber(lock_ttl)+current_time 45 | return ret; 46 | end 47 | ret[1]=0 48 | local st_key=cl_key .. '-st' 49 | local amount=tonumber(ARGV[1]) 50 | local st_expire_ms=tonumber(ARGV[2]) 51 | local period_ms=tonumber(ARGV[3]) 52 | local period_number=tonumber(ARGV[4]) 53 | local cal_start_time=tonumber(ARGV[6]) 54 | local limit_number=tonumber(ARGV[7]) 55 | local lock_seconds=tonumber(ARGV[8]) 56 | local period_expire_ms=st_expire_ms+16 57 | local cur_period 58 | local cur_period_key 59 | local start_time=redis.call('get',st_key) 60 | if(start_time==false) 61 | then 62 | start_time=cal_start_time 63 | cur_period=start_time+period_ms-1 64 | cur_period_key=cl_key .. '-' .. cur_period 65 | redis.call('set',st_key,start_time,'PX',st_expire_ms) 66 | redis.call('set',cur_period_key,amount,'PX',period_expire_ms) 67 | ret[2]=amount 68 | ret[3]=cur_period+1 69 | return ret 70 | end 71 | 72 | start_time=tonumber(start_time) 73 | local past_ms=current_time-start_time 74 | local past_period_number=past_ms/period_ms 75 | local past_period_number_floor=math.floor(past_period_number) 76 | local past_period_number_ceil=math.ceil(past_period_number) 77 | 78 | local past_period_number_fixed=past_period_number_floor 79 | if (past_period_number_ceil>past_period_number_floor) 80 | then 81 | past_period_number_fixed=past_period_number_ceil 82 | end 83 | if past_period_number_fixed==0 84 | then 85 | past_period_number_fixed=1 86 | end 87 | cur_period=start_time + past_period_number_fixed * period_ms - 1 88 | cur_period_key=cl_key .. '-' .. cur_period 89 | 90 | local periods={cur_period_key} 91 | for i=1,period_number-1,1 do 92 | periods[i+1]=cl_key .. '-' .. (cur_period - period_ms * i) 93 | end 94 | local periods_amount=0 95 | local periods_amount_array=redis.call('mget',unpack(periods)) 96 | for key,value in ipairs(periods_amount_array) do 97 | if(value~=false) 98 | then 99 | periods_amount=periods_amount+value 100 | end 101 | end 102 | 103 | ret[2]=amount+periods_amount 104 | ret[3]=cur_period+1 105 | 106 | if (limit_number>=0 and ret[2]>limit_number) then 107 | if lock_seconds>0 then 108 | redis.call('set',lock_key,'1','EX',lock_seconds,'NX') 109 | ret[3]=lock_seconds*1000 110 | end 111 | ret[1]=1 112 | ret[2]=periods_amount 113 | return ret 114 | end 115 | 116 | local current_amount 117 | current_amount = redis.call('incrby',cur_period_key,amount) 118 | current_amount = tonumber(current_amount) 119 | if current_amount == amount then 120 | redis.call('PEXPIRE',cur_period_key,period_expire_ms) 121 | redis.call('PEXPIRE',st_key,st_expire_ms) 122 | end 123 | return ret"); 124 | } 125 | 126 | /// 127 | /// check single rule for target 128 | /// 129 | /// 130 | /// 131 | /// 132 | protected override RuleCheckResult CheckSingleRule(string target, RateLimitRule rule) 133 | { 134 | var currentRule = rule as SlidingWindowRule; 135 | var amount = 1; 136 | 137 | var currentTime = _timeProvider.GetCurrentUtcMilliseconds(); 138 | var startTime = AlgorithmStartTime.ToSpecifiedTypeTime(currentTime, currentRule.StatWindow, currentRule.StartTimeType); 139 | long expireMilliseconds = ((long)currentRule.StatWindow.TotalMilliseconds) * 2; 140 | long periodMilliseconds = (long)currentRule.StatPeriod.TotalMilliseconds; 141 | 142 | var ret = (long[])EvaluateScript(_slidingWindowIncrementLuaScript, 143 | new RedisKey[] { target }, 144 | new RedisValue[] { amount, expireMilliseconds, periodMilliseconds, currentRule.PeriodNumber, currentTime, startTime, currentRule.LimitNumber, currentRule.LockSeconds }); 145 | 146 | return new RuleCheckResult() 147 | { 148 | IsLimit = ret[0] == 0 ? false : true, 149 | Target = target, 150 | Count = ret[1], 151 | Remaining=currentRule.GetLimitThreshold()-ret[1], 152 | Rule = rule, 153 | ResetTime = DateTimeOffset.FromUnixTimeMilliseconds(ret[2]).ToLocalTime(), 154 | }; 155 | } 156 | 157 | /// 158 | /// 159 | /// 160 | /// 161 | /// 162 | /// 163 | protected override async Task CheckSingleRuleAsync(string target, RateLimitRule rule) 164 | { 165 | var currentRule = rule as SlidingWindowRule; 166 | var amount = 1; 167 | 168 | var currentTime = await _timeProvider.GetCurrentUtcMillisecondsAsync().ConfigureAwait(false); 169 | var startTime = AlgorithmStartTime.ToSpecifiedTypeTime(currentTime, currentRule.StatWindow, currentRule.StartTimeType); 170 | long expireMilliseconds = ((long)currentRule.StatWindow.TotalMilliseconds) * 2; 171 | long periodMilliseconds = (long)currentRule.StatPeriod.TotalMilliseconds; 172 | 173 | var ret = (long[])await EvaluateScriptAsync(_slidingWindowIncrementLuaScript, 174 | new RedisKey[] { target }, 175 | new RedisValue[] { amount, expireMilliseconds, periodMilliseconds, currentRule.PeriodNumber, currentTime, startTime, currentRule.LimitNumber, currentRule.LockSeconds }) 176 | .ConfigureAwait(false); 177 | return new RuleCheckResult() 178 | { 179 | IsLimit = ret[0] == 0 ? false : true, 180 | Target = target, 181 | Count = ret[1], 182 | Remaining=currentRule.GetLimitThreshold()-ret[1], 183 | Rule = rule, 184 | ResetTime = DateTimeOffset.FromUnixTimeMilliseconds(ret[2]), 185 | }; 186 | } 187 | } 188 | } --------------------------------------------------------------------------------