1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
-- GCRA (Generic Cell Rate Algorithm) check for RedisStorage.
--
-- Redis runs the whole script atomically: no other command touches the key
-- while it executes, so concurrent requests from any instance are safe.
-- The Rust reference implementation is `check_gcra` in src/gcra.rs. Two
-- test suites compare this script's results with it:
-- cargo test --features redis --lib script_tests (embedded Lua 5.1, no
-- Redis server needed)
-- cargo test --features redis --test redis_tests (a real Redis)
--
-- Input
-- KEYS[1] bucket key: one user within one tier.
-- ARGV[1] current time in microseconds since the Unix epoch, as an integer
-- string. An empty string means "read it with redis.call('TIME')",
-- which is the default so every instance shares Redis's clock.
-- ARGV[2] emission interval in microseconds (time per request), >= 1.
-- ARGV[3] burst offset in microseconds (emission interval * max burst).
-- ARGV[4] cost of this request in cells, >= 0. It is never above the max
-- burst: the Rust side rejects that before calling the script.
--
-- Stored value
-- The TAT (theoretical arrival time) in microseconds, as an integer string.
-- A missing key, or a TAT in the past, means a fresh bucket.
--
-- Output: an array of four integers
-- { allowed, remaining, retry_after_us, reset_after_us }
-- allowed 1 if the request is allowed, 0 if it is rate limited.
-- remaining requests left after this one (0 when limited).
-- retry_after_us wait before this request would be allowed (0 when allowed).
-- reset_after_us time until the bucket is full again. When limited, measure
-- it from the stored TAT: the rejected request consumed
-- nothing.
--
-- Effects
-- allowed store the new TAT with SET key value PX ms, where ms is the
-- time until the bucket is full again, rounded UP: rounding down
-- would let the key expire before the TAT and grant requests
-- early. PX must be >= 1, so store nothing when the new TAT is
-- not in the future (a cost of 0 on a fresh bucket).
-- limited write nothing, except to store a capped TAT (see Safety).
--
-- Safety
-- * Every write carries an expiry, so no key outlives its bucket.
-- * Only KEYS[1] is read or written (required by Redis Cluster).
-- * Malformed arguments, or values past the exact range of Lua numbers,
-- return an error reply without writing. The message never includes the
-- key or the arguments, since plain keys contain user ids.
-- * A stored value that is not an integer, or a key holding another type
-- (WRONGTYPE), counts as a fresh bucket and is overwritten, so a corrupted
-- key heals itself instead of failing every request for that user. Keys
-- under the RedisStorage prefix belong to it.
-- * A TAT further ahead than one full burst can only come from a clock that
-- moved back (for example, a failover to a replica whose clock is behind).
-- It is capped at a full bucket and the cap is stored, even when the
-- request is limited: otherwise every retry would meet the old TAT again
-- and the user would stay locked out for as long as the clock moved back.
--
-- Pitfalls
-- * Lua numbers are doubles, exact for integers up to 2^53. That covers
-- microseconds until the year 2255; nanoseconds would lose precision.
-- * Numbers passed straight to redis.call() or returned from the script
-- are converted exactly by Redis. Lua's own conversions are not:
-- tostring(n) and "x" .. n use "%.14g", so a 16-digit TAT becomes
-- "1.7900000011235e+15". This script never converts numbers itself.
-- * Calling TIME before a write needs effects replication: the default
-- since Redis 5 and the only mode since Redis 7. The guarded
-- redis.replicate_commands() call enables it on Redis 3.2 to 4.
-- Every integer below this is exact in a Lua 5.1 number (a double).
local MAX_EXACT = 2 ^ 53
local
-- A non-negative integer in the exact range, or nil. tonumber() in Lua 5.1
-- also accepts "nan", "inf" and fractions, which are all rejected here.
local
if #KEYS ~= 1 or #ARGV ~= 4
local emission_interval = exact_integer
if emission_interval == nil or emission_interval < 1
local burst_offset = exact_integer
if burst_offset == nil or burst_offset < emission_interval
local cost = exact_integer
if cost == nil
local now
if ARGV == ""
if now == nil
-- A key of another type makes GET fail; pcall turns that into an error
-- table, which tonumber() reads as nil. Not an integer (this includes NaN)
-- means a corrupted value too: start fresh.
local stored = redis.
if type == "table"
local tat = tonumber
local capped = false
if tat == nil or tat ~= math. or tat < now tat > now + burst_offset
local increment = emission_interval * cost
if increment > MAX_EXACT - tat
local new_tat = tat + increment
local allow_at = new_tat - burst_offset
if allow_at > now
if new_tat > now
local remaining = math.
return