Coder Social home page Coder Social logo

cristipufu / aspnetcore-redis-rate-limiting Goto Github PK

View Code? Open in Web Editor NEW
193.0 3.0 20.0 556 KB

Set up a Redis backplane for ASP.NET Core multi-node deployments, using the built-in Rate Limiting support that's part of .NET 7 and .NET 8.

License: MIT License

C# 100.00%
asp-net-core aspnetcore net7 rate-limit rate-limiter rate-limiting redis distributed rate-limit-redis rate-limiter-api

aspnetcore-redis-rate-limiting's People

Contributors

altso avatar cristipufu avatar dependabot[bot] avatar hacst avatar jacksga avatar kamilslusarczykdotdigital avatar namoshek avatar robert-ursu avatar swintdc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

aspnetcore-redis-rate-limiting's Issues

Distributed system concurency

Hello,

That is a great package. I have tested with multiple instances of same api behind a loadbalancer. It seems to be working well. But have you tried it with high volume of concurent/paralel requests. Is the counter atomic? And also when you hit the limit, it increments the counter value by number of instances.
Also , its response time is above 2000ms when using client based limiter with multiple instances of app.
Thanks :)

Redis Error: WRONGTYPE Operation against a key holding the wrong kind of value

I wanted to use this library and did some testing. This was my code:

`services.AddRateLimiter(options =>
{
options.OnRejected = (context, _) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}

    context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
    context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");

    return new ValueTask();
};
options.GlobalLimiter = PartitionedRateLimiter.CreateChained(
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            string clientId = GetClientFromContext(httpContext);
            return RedisRateLimitPartition.GetFixedWindowRateLimiter(clientId, _ =>
               new RedisFixedWindowRateLimiterOptions
               {
                   ConnectionMultiplexerFactory = () => connectionMultiplexer,
                   PermitLimit = 2,
                   Window = TimeSpan.FromSeconds(1)                                   
               });
        }),
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            string clientId = GetClientFromContext(httpContext);
            return RateLimitPartition.GetNoLimiter(clientId);
        }),
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            string clientId = GetClientFromContext(httpContext);
            return RedisRateLimitPartition.GetConcurrencyRateLimiter(clientId, _ =>
               new RedisConcurrencyRateLimiterOptions
               {
                   ConnectionMultiplexerFactory = () => connectionMultiplexer,
                   PermitLimit = 5,
                   QueueLimit = 10
               });
        })
        );

});`

It worked as expected for a while, and at some point I started getting this error from Redis:

"ERR Error running script (call to f_a08ae7b80fbefc1d082f3c02f112bb4f38a59fa7): @user_script:8: WRONGTYPE Operation against a key holding the wrong kind of value"

I looked in Redis monitor and saw the error as a result of this command:
"evalsha" "a08ae7b80fbefc1d082f3c02f112bb4f38a59fa7" "3" "rl:{tabdevweb.ini}" "rl:{tabdevweb.ini}:q" "rl:{tabdevweb.ini}:stats" "2" "1000" "0" "1693226580" "rl:{tabdevweb.ini}" "rl:{tabdevweb.ini}:q" "b2ec810f-80ac-4c50-b2b9-e46aad0de33c" "rl:{tabdevweb.ini}:stats"

After a while (next day) the error disappeared and now its working fine again.

Thanks

RedisSlidingWindowRateLimiter breaks if configured with sub-second precision Window

RedisSlidingWindowManager uses the EXPIREAT redis call to update the expiry every time it attempts to acquire a lease. This function only accepts a whole number of seconds as its argument. However the expression (RedisValue)_options.Window.TotalSeconds, is used when passing the window size into the lua script where TotalSeconds is a double.

This usually works because RedisValue turns double into an integer if it can be exactly represented. But if a window size with sub-second precision is specified, the value is passed as a double and an error occurs.

The obvious fix is to just cast total seconds to long to truncate it before conversion to a RedisValue. Alternatively if sub-second precision is actually wanted Redis also supports millisecond precision for expiry through PEXPIREAT since version 2.6.0.

If I know which option is preferred I can create a PR.

Lua script attempted to access a non local key in a cluster node

Using AWS ElasticCache:

StackExchange.Redis.RedisServerException: ERR Error running script (call to f_d3a858ae047422548b35753d09e9d2fe57ec91c0): @user_script:2: @user_script: 2: Lua script attempted to access a non local key in a cluster node
   at StackExchange.Redis.ConnectionMultiplexer.ExecuteSyncImpl[T](Message message, ResultProcessor`1 processor, ServerEndPoint server, T defaultValue) in /_/src/StackExchange.Redis/ConnectionMultiplexer.cs:line 1909
   at StackExchange.Redis.RedisDatabase.ScriptEvaluate(String script, RedisKey[] keys, RedisValue[] values, CommandFlags flags) in /_/src/StackExchange.Redis/RedisDatabase.cs:line 1501
   at StackExchange.Redis.RedisDatabase.ScriptEvaluate(LuaScript script, Object parameters, CommandFlags flags) in /_/src/StackExchange.Redis/RedisDatabase.cs:line 1536
   at RedisRateLimiting.Concurrency.RedisFixedWindowManager.TryAcquireLease()
   at RedisRateLimiting.RedisFixedWindowRateLimiter`1.AttemptAcquireCore(Int32 permitCount)

Rate limiter causes thread pool exhaustion

When trying to load test the sliding window limiter I noticed that it quickly lead to redis timeouts especially in environments with limited cpu count and/or and a bit of latency to redis. This seems to be caused by exhausting the thread pool. Looking into the issue I found the following: The TryAcquireAsync function in RateLimitingMiddleware.cs first does a synchronous AttemptAcquire call before falling back to before falling back to AcquireAsync on the RateLimiter. This means unless something goes wrong, only synchronous StackExchange Redis calls are performed by this package.

Looking at the documentation on the RateLimiter class it says:

AcquireAsync(Int32, CancellationToken) Wait until the requested permits are available or permits can no longer be acquired.
AttemptAcquire(Int32) Fast synchronous attempt to acquire permits.

So this isn't just the usual case of having an async and a blocking implementation but meant to be distinct functions that can both be used.

I have to admit that I am not sure I fully get the intent of the interface with regards to waiting until a permit is available. But I think the way to get proper scalability would probably be to never return a lease from the synchronous call and only reach out to redis in the asynchronous one.

Would this be an appropriate change? Bit surprised I am the first one to stumble over this so maybe I am on the wrong track completely.

[Feature Request] .Net 8 support

Hi @cristipufu,

My team is preparing to upgrade from .Net6 to .Net8 (due for GA in November) and I noticed this library is .Net7 only. Do you have any plans to add .Net8 support soon?

If not, is there anything holding you back where perhaps we could be of help?

Cheers, Jeroen

RedisTokenBucketRateLimiter on AWS Serverless Redis Cache

RedisTokenBucketRateLimiter throws an error on Amazon ElastiCache Serverless for Redis. Every other limiter type works just fine.

StackExchange.Redis.RedisServerException: ERR This Redis command is not allowed from script script: 53aa7d296ba9ded783301cc275161b6e344ad383, on @user_script:13.
      at StackExchange.Redis.RedisDatabase.ScriptEvaluateAsync(String script, RedisKey[] keys, RedisValue[] values, CommandFlags flags) in /_/src/StackExchange.Redis/RedisDatabase.cs:line 1551
      at RedisRateLimiting.Concurrency.RedisTokenBucketManager.TryAcquireLeaseAsync()
      at RedisRateLimiting.RedisTokenBucketRateLimiter`1.AcquireAsyncCoreInternal()

Handle Redis not available

What happens if we can't connect to Redis?
Should we fail the request?
Should we swallow the exception?
Should we configure the behavior?

The RedisTokenBucketRateLimiter refills the bucket immediately after some timeout

According to the documentation, the RedisTokenBucketRateLimiter is supposed to refill the bucket by TokensPerPeriod every ReplenishmentPeriod until TokenLimit is reached. Because refilling the bucket is no constant process, this is done whenever the rate limit is accessed through the Lua script.

To prevent stale rate limit entries in Redis, the Lua script calculates and sets a TTL for the rate limit keys:

local fill_time = limit / rate
local ttl = math.floor(fill_time * 2)

The calculated TTL, however, does not seem to be correct. fill_time is the number of ReplenishmentPeriods it takes, until the bucket is full again. This leads to a valid TTL if ReplenishmentPeriod is less than 2 seconds. But in case the ReplenishmentPeriod is higher, like 15 seconds, the TTL is wrong. Which, in case no requests occur for the duration of the TTL, leads to a bucket which is filled early.

Example of a problematic rate limiter:

builder.Services.AddRateLimiter(options =>
{
    options.AddRedisTokenBucketLimiter("MyLimit", (opt) =>
    {
        opt.ConnectionMultiplexerFactory = () => connectionMultiplexer;
        opt.TokenLimit = 3;
        opt.TokensPerPeriod = 1;
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(15);
    });
});

The fix for this should be simple, but I'll have to look into it.

RedisSlidingWindowRateLimiter does not provide IdleDuration

The current implementation always returns TimeSpan.Zero for RateLimiter.IdleDuration. When using a partitioned rate limiter this means the rate limiters created for the partitions never get cleaned up and will accumulate.

I think this is true for all rate limiters in this repo however as it is the only one I am using right now, I only looked at the sliding window rate limiter in detail so far. For that the most reasonable approach to me seems to be to accept some imprecision and just track the last expireat value set in the RedisSlidingWindowManager and provide an estimated idle duration for that. This is under the assumption that if a manager ever deletes a partition that was not actually fully idle, it is really no problem to have it just be re-created by the factory. The actual limit is still in redis after all.

I could create a pull request for the redis sliding rate limiter if this approach is acceptable.

This would not fully match what the interface wants though. The documentation says:

Specifies how long the RateLimiter has had all permits available.

A better estimate or precise answer could of course be given by reaching out to redis, however to perform potentially blocking operations in there seems problematic to me.

permitCount paramater values larger than 1 are currently not supported

The parameter permitCount is passed to both AttemptAcquire and AcquireAsync in the RateLimiter abstract base class that all rate limiters in this library are are implementing.

The definition for this paramater is as follows:
<param name="permitCount">Number of permits to try and acquire.</param>
(See for instance here: https://github.com/dotnet/runtime/blob/43a60c8ed073a4c6134facadd01c9c1c2643e41a/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs#L60)

Yet, all the provided classes disregard this parameter value, as in here:

protected override ValueTask<RateLimitLease> AcquireAsyncCore(int permitCount, CancellationToken cancellationToken)
{
if (permitCount > _options.PermitLimit)
{
throw new ArgumentOutOfRangeException(nameof(permitCount), permitCount, string.Format("{0} permit(s) exceeds the permit limit of {1}.", permitCount, _options.PermitLimit));
}
return AcquireAsyncCoreInternal();
}

In some cases, a hard coded value of 1D is then passed on instead of the parameter, as in here:

var response = (RedisValue[]?)await database.ScriptEvaluateAsync(
_redisScript,
new
{
rate_limit_key = RateLimitKey,
expires_at_key = RateLimitExpireKey,
next_expires_at = now.Add(_options.Window).ToUnixTimeSeconds(),
current_time = nowUnixTimeSeconds,
increment_amount = 1D,
});
var result = new RedisFixedWindowResponse();

Are there plans to solve this?
Also, If this is currently a known limitation of this library (fair), please provide a warning in the documentation.

Thanks.

Chained limiters

Hi,

Will it be possible to manage chained limiters in future releases?

Thanks in advance.

Unable to get whether permits are exhausted / wait until permits are replenished

According to the definitions of AttemptAcquire and AcquireAsync, when given permitCount = 0, it is possible to check if the permits are exhausted or wait until the permits are replenished, respectively.

Here are the links to the definitions:

https://learn.microsoft.com/en-us/dotnet/api/system.threading.ratelimiting.ratelimiter.attemptacquire?view=aspnetcore-7.0

https://learn.microsoft.com/en-us/dotnet/api/system.threading.ratelimiting.ratelimiter.acquireasync?view=aspnetcore-7.0

I would be happy to know if there is any workaround to check the actual state of the permits without acquiring a lease.

[Feature Request] Adding Rules based on config?

Being able to apply Rules to a controller's action by just defining endpoint and limit settings in appsettings.json similar to how it is done in AspNetCoreRateLimit.

Want to be able to dynamically assign rules to each action instead of having to manually doing it (as some projects are too big to do it manually)

Configurable prefix for redis key

Hey,

thanks for providing this useful package. I was wondering if there is a way to configure a prefix for the redis key? Currently, it could lead to problems if several applications share the same redis database.

Thanks in advance.

Send rate limit headers when request is successful

Custom middleware

public interface IRateLimiterPolicy<TPartitionKey>
{
    Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; }
+   Func<OnAcquiredContext, CancellationToken, ValueTask>? OnAcquired { get; }
}
public sealed class RateLimiterOptions
{
    public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; set; }
+    public Func<OnAcquiredContext, CancellationToken, ValueTask>? OnAcquired { get; set; }
}

dotnet/aspnetcore#44140

Add metadata to also send header X-Rate-Limit-Reset

A header "X-Rate-Limit-Reset" containing for example "2023-04-21T11:21:43.6820378Z" would be a nice addition.

Since the data is available on the RedisFixedWindowResponse class, it should be possible to add it.

See pull request: #55

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.