Files
upstream-zulip/zerver/lib/rate_limiter.lua
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

101 lines
3.4 KiB
Lua

-- GCRA (Generic Cell Rate Algorithm) rate limiter.
-- Atomically checks all rules and updates TATs in a single round trip.
--
-- KEYS[1] = block_key, KEYS[2] = gcra_key
-- ARGV[1] = num_rules, ARGV[2..2*num_rules+1] = w1, l1, w2, l2, ...
-- ARGV[2*num_rules+2] (optional) = override timestamp for tests only
--
-- Returns a 4-element list of strings:
-- [ratelimited, secs_to_freedom, calls_remaining, secs_to_reset]
local block_key = KEYS[1]
local gcra_key = KEYS[2]
local num_rules = tonumber(ARGV[1])
-- Use Redis server time so that all app servers see a consistent
-- clock, avoiding false positives from inter-machine clock drift.
-- Tests may pass an explicit timestamp as the last argument to get
-- deterministic behavior without wall-clock sleeps.
local now
local test_time_arg = ARGV[2 * num_rules + 2]
if test_time_arg then
now = tonumber(test_time_arg)
else
local time_result = redis.call('TIME')
now = tonumber(time_result[1]) + tonumber(time_result[2]) / 1000000
end
-- Check for manual block (set by block_access).
if redis.call('EXISTS', block_key) == 1 then
local ttl = redis.call('PTTL', block_key)
if ttl < 0 then ttl = 1000 end
return {'1', string.format('%.17g', ttl / 1000.0), '0', '0'}
end
-- For each rule, compute the new TAT (Theoretical Arrival Time)
-- without writing anything yet. If any rule would be violated,
-- we reject the request and leave all TATs unchanged.
local fields = {}
local new_tats = {}
local max_ttl = 0
local limited = false
local secs_to_freedom = 0
for i = 1, num_rules do
local window = tonumber(ARGV[1 + (i - 1) * 2 + 1])
local limit = tonumber(ARGV[1 + (i - 1) * 2 + 2])
local field = window .. ':' .. limit
-- Time between ideally-spaced requests.
local emission_interval = window / limit
local stored = redis.call('HGET', gcra_key, field)
-- If the stored TAT is in the past, treat it as now (the
-- bucket has fully drained).
local tat = math.max(stored and tonumber(stored) or now, now)
local new_tat = tat + emission_interval
-- The bucket overflows when new_tat exceeds the window
-- horizon: the request arrived faster than the drain rate
-- can sustain.
if new_tat > now + window then
limited = true
local freedom = new_tat - window - now
if freedom > secs_to_freedom then
secs_to_freedom = freedom
end
end
fields[i] = field
new_tats[i] = new_tat
local remaining = new_tat - now
if remaining > max_ttl then
max_ttl = remaining
end
end
-- All-or-nothing: if any rule triggered, reject without updating.
if limited then
return {'1', string.format('%.17g', secs_to_freedom), '0', '0'}
end
-- Request allowed — persist all new TATs atomically.
for i = 1, num_rules do
redis.call('HSET', gcra_key, fields[i], string.format('%.17g', new_tats[i]))
end
local ttl_seconds = math.ceil(max_ttl)
if ttl_seconds > 0 then
redis.call('EXPIRE', gcra_key, ttl_seconds)
end
-- Compute remaining quota for the max (outermost) rule, used for
-- the X-RateLimit-Remaining response header.
local max_window = tonumber(ARGV[1 + (num_rules - 1) * 2 + 1])
local max_limit = tonumber(ARGV[1 + (num_rules - 1) * 2 + 2])
local max_tat = new_tats[num_rules]
local calls_remaining = math.max(0, math.floor(
(now + max_window - max_tat) * max_limit / max_window
))
local secs_to_reset = max_tat - now
return {'0', '0', tostring(calls_remaining), string.format('%.17g', secs_to_reset)}