snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
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
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
This commit is contained in:
100
zerver/lib/rate_limiter.lua
Normal file
100
zerver/lib/rate_limiter.lua
Normal file
@@ -0,0 +1,100 @@
|
||||
-- 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)}
|
||||
Reference in New Issue
Block a user