Skip to content

Commit 9da2fcb

Browse files
committed
feat: add limit-count sliding window for redis with tests
Signed-off-by: Sihyeon Jang <[email protected]>
1 parent 984e8aa commit 9da2fcb

File tree

2 files changed

+515
-9
lines changed

2 files changed

+515
-9
lines changed

apisix/plugins/limit-count/limit-count-redis.lua

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ local setmetatable = setmetatable
2121
local tostring = tostring
2222

2323

24-
local _M = {version = 0.3}
24+
local _M = {version = 0.4}
2525

2626

2727
local mt = {
2828
__index = _M
2929
}
3030

3131

32-
local script = core.string.compress_script([=[
32+
local script_fixed = core.string.compress_script([=[
3333
assert(tonumber(ARGV[3]) >= 1, "cost must be at least 1")
3434
local ttl = redis.call('ttl', KEYS[1])
3535
if ttl < 0 then
@@ -40,12 +40,61 @@ local script = core.string.compress_script([=[
4040
]=])
4141

4242

43-
function _M.new(plugin_name, limit, window, conf)
43+
local script_sliding = core.string.compress_script([=[
44+
assert(tonumber(ARGV[3]) >= 1, "cost must be at least 1")
45+
46+
local now = tonumber(ARGV[1])
47+
local window = tonumber(ARGV[2])
48+
local limit = tonumber(ARGV[3])
49+
local cost = tonumber(ARGV[4])
50+
51+
local window_start = now - window
52+
53+
-- remove events outside of the window
54+
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, window_start)
55+
56+
local current = redis.call('ZCARD', KEYS[1])
57+
58+
if current + cost > limit then
59+
local earliest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES')
60+
local reset = 0
61+
if #earliest == 2 then
62+
reset = earliest[2] + window - now
63+
if reset < 0 then
64+
reset = 0
65+
end
66+
end
67+
return {-1, reset}
68+
end
69+
70+
for i = 1, cost do
71+
redis.call('ZADD', KEYS[1], now, now .. ':' .. i)
72+
end
73+
74+
redis.call('PEXPIRE', KEYS[1], window)
75+
76+
local remaining = limit - (current + cost)
77+
78+
local earliest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES')
79+
local reset = 0
80+
if #earliest == 2 then
81+
reset = earliest[2] + window - now
82+
if reset < 0 then
83+
reset = 0
84+
end
85+
end
86+
87+
return {remaining, reset}
88+
]=])
89+
90+
91+
function _M.new(plugin_name, limit, window, window_type, conf)
4492
assert(limit > 0 and window > 0)
4593

4694
local self = {
4795
limit = limit,
4896
window = window,
97+
window_type = window_type or "fixed",
4998
conf = conf,
5099
plugin_name = plugin_name,
51100
}
@@ -59,13 +108,22 @@ function _M.incoming(self, key, cost)
59108
return red, err, 0
60109
end
61110

62-
local limit = self.limit
63-
local window = self.window
64-
local res
65111
key = self.plugin_name .. tostring(key)
66112

67113
local ttl = 0
68-
res, err = red:eval(script, 1, key, limit, window, cost or 1)
114+
local limit = self.limit
115+
local c = cost or 1
116+
local res
117+
118+
if self.window_type == "sliding" then
119+
local now = ngx.now() * 1000
120+
local window = self.window * 1000
121+
122+
res, err = red:eval(script_sliding, 1, key, now, window, limit, c)
123+
else
124+
local window = self.window
125+
res, err = red:eval(script_fixed, 1, key, limit, window, c)
126+
end
69127

70128
if err then
71129
return nil, err, ttl
@@ -74,14 +132,15 @@ function _M.incoming(self, key, cost)
74132
local remaining = res[1]
75133
ttl = res[2]
76134

77-
local ok, err = red:set_keepalive(10000, 100)
135+
local ok, err2 = red:set_keepalive(10000, 100)
78136
if not ok then
79-
return nil, err, ttl
137+
return nil, err2, ttl
80138
end
81139

82140
if remaining < 0 then
83141
return nil, "rejected", ttl
84142
end
143+
85144
return 0, remaining, ttl
86145
end
87146

0 commit comments

Comments
 (0)