Skip to content

Commit b82b8bb

Browse files
samugiwindmgc
authored andcommitted
fix(rate-limiting): counters accuracy with redis policy & sync_rate (#11859)
* fix(rate-limiting): redis async updates When the periodic sync to redis feature is turned on, using the `sync_rate` configuration option, keys are incremented by steps of 2 instead of 1 for requests that arrive after the `sync_rate` interval has expired. This happens because after each sync, the key is loaded again from redis and also incremented atomically (see: #10559) however the next call to `increment` also adds 1 to its value, so the key is incremented by 2 every time it's loaded from redis. This fix sets a negative delta for the key when `conf.sync_rate ~= SYNC_RATE_REALTIME` and the key was loaded from redis in order to invalidate the next call to `increment`. Includes a small code refactor
1 parent a2cea88 commit b82b8bb

File tree

3 files changed

+57
-47
lines changed

3 files changed

+57
-47
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
message: "**Rate Limiting**: fix to provide better accuracy in counters when sync_rate is used with the redis policy."
2+
type: bugfix
3+
scope: Plugin

kong/plugins/rate-limiting/policies/init.lua

+4-7
Original file line numberDiff line numberDiff line change
@@ -206,14 +206,9 @@ local function update_local_counters(conf, periods, limits, identifier, value)
206206
if limits[period] then
207207
local cache_key = get_local_key(conf, identifier, period, period_date)
208208

209-
if cur_delta[cache_key] then
210-
cur_delta[cache_key] = cur_delta[cache_key] + value
211-
else
212-
cur_delta[cache_key] = value
213-
end
209+
cur_delta[cache_key] = (cur_delta[cache_key] or 0) + value
214210
end
215211
end
216-
217212
end
218213

219214
return {
@@ -346,7 +341,9 @@ return {
346341
if conf.sync_rate ~= SYNC_RATE_REALTIME then
347342
cur_usage[cache_key] = current_metric or 0
348343
cur_usage_expire_at[cache_key] = periods[period] + EXPIRATION[period]
349-
cur_delta[cache_key] = 0
344+
-- The key was just read from Redis using `incr`, which incremented it
345+
-- by 1. Adjust the value to account for the prior increment.
346+
cur_delta[cache_key] = -1
350347
end
351348

352349
return current_metric or 0

spec/03-plugins/23-rate-limiting/02-policies_spec.lua

+50-40
Original file line numberDiff line numberDiff line change
@@ -176,53 +176,63 @@ describe("Plugin: rate-limiting (policies)", function()
176176
end)
177177
end
178178

179-
for _, sync_rate in ipairs{1, SYNC_RATE_REALTIME} do
180-
describe("redis with sync rate: " .. sync_rate, function()
181-
local identifier = uuid()
182-
local conf = {
183-
route_id = uuid(),
184-
service_id = uuid(),
185-
redis_host = helpers.redis_host,
186-
redis_port = helpers.redis_port,
187-
redis_database = 0,
188-
sync_rate = sync_rate,
189-
}
190-
191-
before_each(function()
192-
local red = require "resty.redis"
193-
local redis = assert(red:new())
194-
redis:set_timeout(1000)
195-
assert(redis:connect(conf.redis_host, conf.redis_port))
196-
redis:flushall()
197-
redis:close()
198-
end)
199-
200-
it("increase & usage", function()
201-
--[[
202-
Just a simple test:
203-
- increase 1
204-
- check usage == 1
205-
- increase 1
206-
- check usage == 2
207-
- increase 1 (beyond the limit)
208-
- check usage == 3
209-
--]]
210-
211-
local current_timestamp = 1424217600
212-
local periods = timestamp.get_timestamps(current_timestamp)
179+
for _, sync_rate in ipairs{0.5, SYNC_RATE_REALTIME} do
180+
local current_timestamp = 1424217600
181+
local periods = timestamp.get_timestamps(current_timestamp)
182+
183+
for period in pairs(periods) do
184+
describe("redis with sync rate: " .. sync_rate .. " period: " .. period, function()
185+
local identifier = uuid()
186+
local conf = {
187+
route_id = uuid(),
188+
service_id = uuid(),
189+
redis_host = helpers.redis_host,
190+
redis_port = helpers.redis_port,
191+
redis_database = 0,
192+
sync_rate = sync_rate,
193+
}
213194

214-
for period in pairs(periods) do
195+
before_each(function()
196+
local red = require "resty.redis"
197+
local redis = assert(red:new())
198+
redis:set_timeout(1000)
199+
assert(redis:connect(conf.redis_host, conf.redis_port))
200+
redis:flushall()
201+
redis:close()
202+
end)
203+
204+
it("increase & usage", function()
205+
--[[
206+
Just a simple test:
207+
- increase 1
208+
- check usage == 1
209+
- increase 1
210+
- check usage == 2
211+
- increase 1 (beyond the limit)
212+
- check usage == 3
213+
--]]
215214

216215
local metric = assert(policies.redis.usage(conf, identifier, period, current_timestamp))
217216
assert.equal(0, metric)
218217

219218
for i = 1, 3 do
220-
assert(policies.redis.increment(conf, { [period] = 2 }, identifier, current_timestamp, 1))
221-
metric = assert(policies.redis.usage(conf, identifier, period, current_timestamp))
222-
assert.equal(i, metric)
219+
-- "second" keys expire too soon to check the async increment.
220+
-- Let's verify all the other scenarios:
221+
if not (period == "second" and sync_rate ~= SYNC_RATE_REALTIME) then
222+
assert(policies.redis.increment(conf, { [period] = 2 }, identifier, current_timestamp, 1))
223+
224+
-- give time to the async increment to happen
225+
if sync_rate ~= SYNC_RATE_REALTIME then
226+
local sleep_time = 1 + (sync_rate > 0 and sync_rate or 0)
227+
ngx.sleep(sleep_time)
228+
end
229+
230+
metric = assert(policies.redis.usage(conf, identifier, period, current_timestamp))
231+
assert.equal(i, metric)
232+
end
223233
end
224-
end
234+
end)
225235
end)
226-
end)
236+
end
227237
end
228238
end)

0 commit comments

Comments
 (0)