Skip to content

Commit 46f402b

Browse files
RedisFixedWindowRateLimiter now supports permitCount parameter
(cherry picked from commit bb6baed024a4595a639085db09df954c90b064bf)
1 parent d6cc72a commit 46f402b

File tree

3 files changed

+51
-25
lines changed

3 files changed

+51
-25
lines changed

src/RedisRateLimiting/FixedWindow/RedisFixedWindowManager.cs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ internal class RedisFixedWindowManager
1313

1414
private static readonly LuaScript _redisScript = LuaScript.Prepare(
1515
@"local expires_at = tonumber(redis.call(""get"", @expires_at_key))
16+
local current = tonumber(redis.call(""get"", @rate_limit_key))
17+
local requested = tonumber(@increment_amount)
18+
local limit = tonumber(@permit_limit)
1619
1720
if not expires_at or expires_at < tonumber(@current_time) then
1821
-- this is either a brand new window,
@@ -27,13 +30,19 @@ internal class RedisFixedWindowManager
2730
redis.call(""expireat"", @expires_at_key, @next_expires_at + 1)
2831
-- since the database was updated, return the new value
2932
expires_at = @next_expires_at
33+
current = 0
3034
end
3135
32-
-- now that the window either already exists or it was freshly initialized,
33-
-- increment the counter(`incrby` returns a number)
34-
local current = redis.call(""incrby"", @rate_limit_key, @increment_amount)
36+
local allowed = current + requested <= limit
3537
36-
return { current, expires_at }");
38+
if allowed
39+
then
40+
-- now that the window either already exists or it was freshly initialized,
41+
-- increment the counter(`incrby` returns a number)
42+
current = redis.call(""incrby"", @rate_limit_key, @increment_amount)
43+
end
44+
45+
return { current, expires_at, allowed }");
3746

3847
public RedisFixedWindowManager(
3948
string partitionKey,
@@ -46,7 +55,7 @@ public RedisFixedWindowManager(
4655
RateLimitExpireKey = new RedisKey($"rl:{{{partitionKey}}}:exp");
4756
}
4857

49-
internal async Task<RedisFixedWindowResponse> TryAcquireLeaseAsync()
58+
internal async Task<RedisFixedWindowResponse> TryAcquireLeaseAsync(int permitCount)
5059
{
5160
var now = DateTimeOffset.UtcNow;
5261
var nowUnixTimeSeconds = now.ToUnixTimeSeconds();
@@ -59,9 +68,10 @@ internal async Task<RedisFixedWindowResponse> TryAcquireLeaseAsync()
5968
{
6069
rate_limit_key = RateLimitKey,
6170
expires_at_key = RateLimitExpireKey,
71+
permit_limit = _options.PermitLimit,
6272
next_expires_at = now.Add(_options.Window).ToUnixTimeSeconds(),
6373
current_time = nowUnixTimeSeconds,
64-
increment_amount = 1D,
74+
increment_amount = permitCount,
6575
});
6676

6777
var result = new RedisFixedWindowResponse();
@@ -70,13 +80,14 @@ internal async Task<RedisFixedWindowResponse> TryAcquireLeaseAsync()
7080
{
7181
result.Count = (long)response[0];
7282
result.ExpiresAt = (long)response[1];
83+
result.Allowed = (bool)response[2];
7384
result.RetryAfter = TimeSpan.FromSeconds(result.ExpiresAt - nowUnixTimeSeconds);
7485
}
7586

7687
return result;
7788
}
7889

79-
internal RedisFixedWindowResponse TryAcquireLease()
90+
internal RedisFixedWindowResponse TryAcquireLease(int permitCount)
8091
{
8192
var now = DateTimeOffset.UtcNow;
8293
var nowUnixTimeSeconds = now.ToUnixTimeSeconds();
@@ -89,9 +100,10 @@ internal RedisFixedWindowResponse TryAcquireLease()
89100
{
90101
rate_limit_key = RateLimitKey,
91102
expires_at_key = RateLimitExpireKey,
103+
permit_limit = _options.PermitLimit,
92104
next_expires_at = now.Add(_options.Window).ToUnixTimeSeconds(),
93105
current_time = nowUnixTimeSeconds,
94-
increment_amount = 1D,
106+
increment_amount = permitCount,
95107
});
96108

97109
var result = new RedisFixedWindowResponse();
@@ -100,6 +112,7 @@ internal RedisFixedWindowResponse TryAcquireLease()
100112
{
101113
result.Count = (long)response[0];
102114
result.ExpiresAt = (long)response[1];
115+
result.Allowed = (bool)response[2];
103116
result.RetryAfter = TimeSpan.FromSeconds(result.ExpiresAt - nowUnixTimeSeconds);
104117
}
105118

@@ -112,5 +125,6 @@ internal class RedisFixedWindowResponse
112125
internal long ExpiresAt { get; set; }
113126
internal TimeSpan RetryAfter { get; set; }
114127
internal long Count { get; set; }
128+
internal bool Allowed { get; set; }
115129
}
116130
}

src/RedisRateLimiting/FixedWindow/RedisFixedWindowRateLimiter.cs

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ protected override ValueTask<RateLimitLease> AcquireAsyncCore(int permitCount, C
5555
throw new ArgumentOutOfRangeException(nameof(permitCount), permitCount, string.Format("{0} permit(s) exceeds the permit limit of {1}.", permitCount, _options.PermitLimit));
5656
}
5757

58-
return AcquireAsyncCoreInternal();
58+
return AcquireAsyncCoreInternal(permitCount);
5959
}
6060

6161
protected override RateLimitLease AttemptAcquireCore(int permitCount)
@@ -71,38 +71,28 @@ protected override RateLimitLease AttemptAcquireCore(int permitCount)
7171
Window = _options.Window,
7272
};
7373

74-
var response = _redisManager.TryAcquireLease();
74+
var response = _redisManager.TryAcquireLease(permitCount);
7575

7676
leaseContext.Count = response.Count;
7777
leaseContext.RetryAfter = response.RetryAfter;
78-
79-
if (leaseContext.Count > _options.PermitLimit)
80-
{
81-
return new FixedWindowLease(isAcquired: false, leaseContext);
82-
}
83-
84-
return new FixedWindowLease(isAcquired: true, leaseContext);
78+
79+
return new FixedWindowLease(isAcquired: response.Allowed, leaseContext);
8580
}
8681

87-
private async ValueTask<RateLimitLease> AcquireAsyncCoreInternal()
82+
private async ValueTask<RateLimitLease> AcquireAsyncCoreInternal(int permitCount)
8883
{
8984
var leaseContext = new FixedWindowLeaseContext
9085
{
9186
Limit = _options.PermitLimit,
9287
Window = _options.Window,
9388
};
9489

95-
var response = await _redisManager.TryAcquireLeaseAsync();
90+
var response = await _redisManager.TryAcquireLeaseAsync(permitCount);
9691

9792
leaseContext.Count = response.Count;
9893
leaseContext.RetryAfter = response.RetryAfter;
9994

100-
if (leaseContext.Count > _options.PermitLimit)
101-
{
102-
return new FixedWindowLease(isAcquired: false, leaseContext);
103-
}
104-
105-
return new FixedWindowLease(isAcquired: true, leaseContext);
95+
return new FixedWindowLease(isAcquired: response.Allowed, leaseContext);
10696
}
10797

10898
private sealed class FixedWindowLeaseContext

test/RedisRateLimiting.Tests/UnitTests/FixedWindowUnitTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,27 @@ public async Task CanAcquireAsyncResource()
8686
using var lease2 = await limiter.AcquireAsync();
8787
Assert.False(lease2.IsAcquired);
8888
}
89+
90+
[Fact]
91+
public async Task SupportsPermitCountFlag()
92+
{
93+
using var limiter = new RedisFixedWindowRateLimiter<string>(
94+
"Test_SupportsPermitCountFlag_FW",
95+
new RedisFixedWindowRateLimiterOptions
96+
{
97+
PermitLimit = 5,
98+
Window = TimeSpan.FromMinutes(1),
99+
ConnectionMultiplexerFactory = Fixture.ConnectionMultiplexerFactory,
100+
});
101+
102+
using var lease = await limiter.AcquireAsync(permitCount: 3);
103+
Assert.True(lease.IsAcquired);
104+
105+
using var lease2 = await limiter.AcquireAsync(permitCount: 3);
106+
Assert.False(lease2.IsAcquired);
107+
108+
using var lease3 = await limiter.AcquireAsync(permitCount: 2);
109+
Assert.True(lease3.IsAcquired);
110+
}
89111
}
90112
}

0 commit comments

Comments
 (0)