Skip to content

Commit 09392df

Browse files
committed
Fixed window support for multi permits
1 parent 23872fa commit 09392df

File tree

3 files changed

+50
-14
lines changed

3 files changed

+50
-14
lines changed

src/RedisRateLimiting/FixedWindow/RedisFixedWindowManager.cs

+24-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ internal class RedisFixedWindowManager
1313

1414
private static readonly LuaScript Script = LuaScript.Prepare(
1515
@"local expires_at = tonumber(redis.call(""get"", @expires_at_key))
16+
local limit = tonumber(@permit_limit)
17+
local inc = tonumber(@increment_amount)
1618
1719
if not expires_at or expires_at < tonumber(@current_time) then
1820
-- this is either a brand new window,
@@ -29,11 +31,25 @@ internal class RedisFixedWindowManager
2931
expires_at = @next_expires_at
3032
end
3133
32-
-- now that the window either already exists or it was freshly initialized,
34+
-- now that the window either already exists or it was freshly initialized
3335
-- increment the counter(`incrby` returns a number)
34-
local current = redis.call(""incrby"", @rate_limit_key, @increment_amount)
3536
36-
return { current, expires_at }");
37+
local current = redis.call(""get"", @rate_limit_key)
38+
39+
if not current then
40+
current = 0
41+
else
42+
current = tonumber(current)
43+
end
44+
45+
local allowed = current + inc <= limit
46+
47+
if allowed then
48+
current = redis.call(""incrby"", @rate_limit_key, inc)
49+
end
50+
51+
return { current, expires_at, allowed }
52+
");
3753

3854
public RedisFixedWindowManager(
3955
string partitionKey,
@@ -46,7 +62,7 @@ public RedisFixedWindowManager(
4662
RateLimitExpireKey = new RedisKey($"rl:fw:{{{partitionKey}}}:exp");
4763
}
4864

49-
internal async Task<RedisFixedWindowResponse> TryAcquireLeaseAsync()
65+
internal async Task<RedisFixedWindowResponse> TryAcquireLeaseAsync(int permitCount)
5066
{
5167
var now = DateTimeOffset.UtcNow;
5268
var nowUnixTimeSeconds = now.ToUnixTimeSeconds();
@@ -61,7 +77,8 @@ internal async Task<RedisFixedWindowResponse> TryAcquireLeaseAsync()
6177
expires_at_key = RateLimitExpireKey,
6278
next_expires_at = (RedisValue)now.Add(_options.Window).ToUnixTimeSeconds(),
6379
current_time = (RedisValue)nowUnixTimeSeconds,
64-
increment_amount = (RedisValue)1D,
80+
permit_limit = (RedisValue)_options.PermitLimit,
81+
increment_amount = (RedisValue)permitCount,
6582
});
6683

6784
var result = new RedisFixedWindowResponse();
@@ -70,6 +87,7 @@ internal async Task<RedisFixedWindowResponse> TryAcquireLeaseAsync()
7087
{
7188
result.Count = (long)response[0];
7289
result.ExpiresAt = (long)response[1];
90+
result.Allowed = (bool)response[2];
7391
result.RetryAfter = TimeSpan.FromSeconds(result.ExpiresAt - nowUnixTimeSeconds);
7492
}
7593

@@ -112,5 +130,6 @@ internal class RedisFixedWindowResponse
112130
internal long ExpiresAt { get; set; }
113131
internal TimeSpan RetryAfter { get; set; }
114132
internal long Count { get; set; }
133+
internal bool Allowed { get; set; }
115134
}
116135
}

src/RedisRateLimiting/FixedWindow/RedisFixedWindowRateLimiter.cs

+4-9
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ protected override ValueTask<RateLimitLease> AcquireAsyncCore(int permitCount, C
5757
throw new ArgumentOutOfRangeException(nameof(permitCount), permitCount, string.Format("{0} permit(s) exceeds the permit limit of {1}.", permitCount, _options.PermitLimit));
5858
}
5959

60-
return AcquireAsyncCoreInternal();
60+
return AcquireAsyncCoreInternal(permitCount);
6161
}
6262

6363
protected override RateLimitLease AttemptAcquireCore(int permitCount)
@@ -66,26 +66,21 @@ protected override RateLimitLease AttemptAcquireCore(int permitCount)
6666
return FailedLease;
6767
}
6868

69-
private async ValueTask<RateLimitLease> AcquireAsyncCoreInternal()
69+
private async ValueTask<RateLimitLease> AcquireAsyncCoreInternal(int permitCount)
7070
{
7171
var leaseContext = new FixedWindowLeaseContext
7272
{
7373
Limit = _options.PermitLimit,
7474
Window = _options.Window,
7575
};
7676

77-
var response = await _redisManager.TryAcquireLeaseAsync();
77+
var response = await _redisManager.TryAcquireLeaseAsync(permitCount);
7878

7979
leaseContext.Count = response.Count;
8080
leaseContext.RetryAfter = response.RetryAfter;
8181
leaseContext.ExpiresAt = response.ExpiresAt;
8282

83-
if (leaseContext.Count > _options.PermitLimit)
84-
{
85-
return new FixedWindowLease(isAcquired: false, leaseContext);
86-
}
87-
88-
return new FixedWindowLease(isAcquired: true, leaseContext);
83+
return new FixedWindowLease(isAcquired: response.Allowed, leaseContext);
8984
}
9085

9186
private sealed class FixedWindowLeaseContext

test/RedisRateLimiting.Tests/UnitTests/FixedWindowUnitTests.cs

+22
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,27 @@ public async Task CanAcquireAsyncResource()
7979
using var lease2 = await limiter.AcquireAsync();
8080
Assert.False(lease2.IsAcquired);
8181
}
82+
83+
[Fact]
84+
public async Task CanAcquireMultiplePermits()
85+
{
86+
using var limiter = new RedisFixedWindowRateLimiter<string>(
87+
partitionKey: Guid.NewGuid().ToString(),
88+
new RedisFixedWindowRateLimiterOptions
89+
{
90+
PermitLimit = 5,
91+
Window = TimeSpan.FromMinutes(1),
92+
ConnectionMultiplexerFactory = Fixture.ConnectionMultiplexerFactory,
93+
});
94+
95+
using var lease = await limiter.AcquireAsync(permitCount: 3);
96+
Assert.True(lease.IsAcquired);
97+
98+
using var lease2 = await limiter.AcquireAsync(permitCount: 3);
99+
Assert.False(lease2.IsAcquired);
100+
101+
using var lease3 = await limiter.AcquireAsync(permitCount: 2);
102+
Assert.True(lease3.IsAcquired);
103+
}
82104
}
83105
}

0 commit comments

Comments
 (0)