Skip to content

Commit d5151b7

Browse files
authored
Merge pull request #50 from kamilslusarczyk/issue/6
Added support for RedisSlidingWindow statistics
2 parents 27cfdf5 + f1ae6c1 commit d5151b7

File tree

3 files changed

+58
-1
lines changed

3 files changed

+58
-1
lines changed

src/RedisRateLimiting/SlidingWindow/RedisSlidingWindowManager.cs

+44
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using StackExchange.Redis;
22
using System;
3+
using System.Threading.RateLimiting;
34
using System.Threading.Tasks;
45

56
namespace RedisRateLimiting.Concurrency
@@ -9,6 +10,7 @@ internal class RedisSlidingWindowManager
910
private readonly IConnectionMultiplexer _connectionMultiplexer;
1011
private readonly RedisSlidingWindowRateLimiterOptions _options;
1112
private readonly RedisKey RateLimitKey;
13+
private readonly RedisKey StatsRateLimitKey;
1214

1315
private static readonly LuaScript _redisScript = LuaScript.Prepare(
1416
@"local limit = tonumber(@permit_limit)
@@ -28,8 +30,22 @@ internal class RedisSlidingWindowManager
2830
2931
redis.call(""expireat"", @rate_limit_key, timestamp + window + 1)
3032
33+
if allowed
34+
then
35+
redis.call(""hincrby"", @stats_key, 'total_successful', 1)
36+
else
37+
redis.call(""hincrby"", @stats_key, 'total_failed', 1)
38+
end
39+
3140
return { allowed, count }");
3241

42+
private static readonly LuaScript StatisticsScript = LuaScript.Prepare(
43+
@"local count = redis.call(""zcard"", @rate_limit_key)
44+
local total_successful_count = redis.call(""hget"", @stats_key, 'total_successful')
45+
local total_failed_count = redis.call(""hget"", @stats_key, 'total_failed')
46+
47+
return { count, total_successful_count, total_failed_count }");
48+
3349
public RedisSlidingWindowManager(
3450
string partitionKey,
3551
RedisSlidingWindowRateLimiterOptions options)
@@ -38,6 +54,7 @@ public RedisSlidingWindowManager(
3854
_connectionMultiplexer = options.ConnectionMultiplexerFactory!.Invoke();
3955

4056
RateLimitKey = new RedisKey($"rl:{{{partitionKey}}}");
57+
StatsRateLimitKey = new RedisKey($"rl:{{{partitionKey}}}:stats");
4158
}
4259

4360
internal async Task<RedisSlidingWindowResponse> TryAcquireLeaseAsync(string requestId)
@@ -54,6 +71,7 @@ internal async Task<RedisSlidingWindowResponse> TryAcquireLeaseAsync(string requ
5471
rate_limit_key = RateLimitKey,
5572
permit_limit = _options.PermitLimit,
5673
window = _options.Window.TotalSeconds,
74+
stats_key = StatsRateLimitKey,
5775
current_time = nowUnixTimeSeconds,
5876
unique_id = requestId,
5977
});
@@ -83,6 +101,7 @@ internal RedisSlidingWindowResponse TryAcquireLease(string requestId)
83101
rate_limit_key = RateLimitKey,
84102
permit_limit = _options.PermitLimit,
85103
window = _options.Window.TotalSeconds,
104+
stats_key = StatsRateLimitKey,
86105
current_time = nowUnixTimeSeconds,
87106
unique_id = requestId,
88107
});
@@ -97,6 +116,31 @@ internal RedisSlidingWindowResponse TryAcquireLease(string requestId)
97116

98117
return result;
99118
}
119+
120+
internal RateLimiterStatistics? GetStatistics()
121+
{
122+
var database = _connectionMultiplexer.GetDatabase();
123+
124+
var response = (RedisValue[]?)database.ScriptEvaluate(
125+
StatisticsScript,
126+
new
127+
{
128+
rate_limit_key = RateLimitKey,
129+
stats_key = StatsRateLimitKey,
130+
});
131+
132+
if (response == null)
133+
{
134+
return null;
135+
}
136+
137+
return new RateLimiterStatistics
138+
{
139+
CurrentAvailablePermits = _options.PermitLimit - (long)response[0],
140+
TotalSuccessfulLeases = (long)response[1],
141+
TotalFailedLeases = (long)response[2],
142+
};
143+
}
100144
}
101145

102146
internal class RedisSlidingWindowResponse

src/RedisRateLimiting/SlidingWindow/RedisSlidingWindowRateLimiter.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public RedisSlidingWindowRateLimiter(TKey partitionKey, RedisSlidingWindowRateLi
4545

4646
public override RateLimiterStatistics? GetStatistics()
4747
{
48-
throw new NotImplementedException();
48+
return _redisManager.GetStatistics();
4949
}
5050

5151
protected override ValueTask<RateLimitLease> AcquireAsyncCore(int permitCount, CancellationToken cancellationToken)

test/RedisRateLimiting.Tests/UnitTests/SlidingWindowUnitTests.cs

+13
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ public async Task ThrowsWhenAcquiringMoreThanLimit()
7878
[Fact]
7979
public async Task CanAcquireAsyncResource()
8080
{
81+
await Fixture.ClearStatisticsAsync("Test_CanAcquireAsyncResource_SW");
82+
8183
using var limiter = new RedisSlidingWindowRateLimiter<string>(
8284
"Test_CanAcquireAsyncResource_SW",
8385
new RedisSlidingWindowRateLimiterOptions
@@ -92,6 +94,17 @@ public async Task CanAcquireAsyncResource()
9294

9395
using var lease2 = await limiter.AcquireAsync();
9496
Assert.False(lease2.IsAcquired);
97+
98+
var stats = limiter.GetStatistics()!;
99+
Assert.Equal(1, stats.TotalSuccessfulLeases);
100+
Assert.Equal(1, stats.TotalFailedLeases);
101+
Assert.Equal(0, stats.CurrentAvailablePermits);
102+
103+
lease.Dispose();
104+
lease2.Dispose();
105+
106+
stats = limiter.GetStatistics()!;
107+
Assert.Equal(0, stats.CurrentAvailablePermits);
95108
}
96109
}
97110
}

0 commit comments

Comments
 (0)