gitlab-labkit 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/labkit/rate_limit/README.md +31 -15
- data/lib/labkit/rate_limit/evaluator.rb +12 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fbd752b05c1341ef1292c7a3a1ae3555dd27230d7afc830e0c713547bdd02ee
|
|
4
|
+
data.tar.gz: 6dc8eab0332b9b6851627fe83a95bfb0bacefbe3222d712e28a8e210c19fdb8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56316fcea7b575a62119a814f3e3b4c85dc8d5e93ed72e2316bd47388bd142dd44ae21f07f2b6f40d070573c04bf6b79c26e14601f90e0a2f6fcf263fa05ce36
|
|
7
|
+
data.tar.gz: 65e7e06919637356e25ab5d6f56880df2eb7e340474afeaf01212fe55612439d8bba8707d2d1bc835c864723cd0d15cca135065756ce3f17a9bd959f253775f3
|
|
@@ -15,7 +15,7 @@ flowchart LR
|
|
|
15
15
|
App[Application code] -->|"check(identifier)"| Limiter
|
|
16
16
|
Limiter -->|delegates| Evaluator
|
|
17
17
|
Evaluator -->|iterates ordered| Rules[Rule list]
|
|
18
|
-
Evaluator <-->|
|
|
18
|
+
Evaluator <-->|EVALSHA / GET / TTL| Redis[(Redis)]
|
|
19
19
|
Evaluator -->|emits| Metrics[Prometheus metrics]
|
|
20
20
|
Evaluator -->|returns| Result
|
|
21
21
|
Result --> App
|
|
@@ -236,9 +236,14 @@ labkit:rl:<limiter_name>:<rule_name>:<char>:<value>[:<char>:<value>...]
|
|
|
236
236
|
|
|
237
237
|
Characteristic values longer than 200 bytes are replaced with a SHA-256
|
|
238
238
|
hexdigest to bound key length. Missing or empty characteristic values are
|
|
239
|
-
encoded as `_unknown_`. The TTL is set on the first write of each window
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
encoded as `_unknown_`. The TTL is set on the first write of each window and
|
|
240
|
+
is not extended on subsequent increments, so the window is a true fixed
|
|
241
|
+
window starting at the first request, not a sliding window.
|
|
242
|
+
|
|
243
|
+
A check is a single `EVALSHA` of `INCR_SCRIPT` (or `SADD_SCRIPT` for
|
|
244
|
+
`count_distinct` rules). Doing the whole read-modify-write inside Lua means
|
|
245
|
+
there is no window between the increment and the `EXPIRE` in which a key
|
|
246
|
+
could be left without a TTL.
|
|
242
247
|
|
|
243
248
|
```mermaid
|
|
244
249
|
sequenceDiagram
|
|
@@ -246,24 +251,35 @@ sequenceDiagram
|
|
|
246
251
|
participant E as Evaluator
|
|
247
252
|
participant P as Connection pool
|
|
248
253
|
participant R as Redis
|
|
254
|
+
participant L as INCR_SCRIPT (Lua)
|
|
249
255
|
|
|
250
256
|
E->>P: pool.with { |conn| ... }
|
|
251
257
|
P-->>E: conn
|
|
252
|
-
E->>R:
|
|
253
|
-
R
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
E->>R: EVALSHA INCR_SCRIPT key, [period, cost]
|
|
259
|
+
R->>L: run script
|
|
260
|
+
L->>L: INCRBYFLOAT key cost
|
|
261
|
+
L->>L: TTL key
|
|
262
|
+
alt TTL < 0 (key was missing, or had no expiry)
|
|
263
|
+
L->>L: EXPIRE key period
|
|
264
|
+
Note over L: Returns period as the TTL:<br/>the window starts now.
|
|
265
|
+
else TTL >= 0 (window already running)
|
|
266
|
+
Note over L: TTL is not extended:<br/>fixed window from first write.
|
|
260
267
|
end
|
|
268
|
+
L-->>R: {count, ttl}
|
|
269
|
+
R-->>E: [count, ttl]
|
|
261
270
|
E-->>P: release conn
|
|
262
271
|
```
|
|
263
272
|
|
|
264
|
-
`
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
Because `INCRBYFLOAT` preserves an existing key's TTL, and creates a missing
|
|
274
|
+
key with no expiry, the TTL only needs reading once — after the increment.
|
|
275
|
+
That single read distinguishes both cases the script must handle, so there is
|
|
276
|
+
no reason to read it again before mutating.
|
|
277
|
+
|
|
278
|
+
`peek` does not use a script: it pipelines `GET` + `TTL` (or `SCARD` + `TTL`)
|
|
279
|
+
and never issues `EXPIRE`, so it cannot start or extend a window. A missing
|
|
280
|
+
key (`GET → nil`, `TTL → -2`) is reported as `count = 0`, and `build_result`
|
|
281
|
+
falls back to the rule's period for `reset_at` since there is no Redis-side
|
|
282
|
+
window to read.
|
|
267
283
|
|
|
268
284
|
## Result
|
|
269
285
|
|
|
@@ -22,40 +22,42 @@ module Labkit
|
|
|
22
22
|
# Redis treats the result as a no-op on the stored value while
|
|
23
23
|
# still observing the post-state count and TTL we return.
|
|
24
24
|
#
|
|
25
|
-
#
|
|
25
|
+
# ttl_after < 0 covers TTL=-2 (key missing) and TTL=-1 (no expiry).
|
|
26
26
|
# The -1 case shouldn't arise with the atomic script, but self-healing
|
|
27
27
|
# recovers keys left without TTL by any prior bug.
|
|
28
28
|
INCR_SCRIPT = Labkit::Redis::Script.new(<<~LUA)
|
|
29
29
|
local ttl = ARGV[1]
|
|
30
30
|
local cost = tonumber(ARGV[2])
|
|
31
|
-
local ttl_before = redis.call('TTL', KEYS[1])
|
|
32
31
|
|
|
33
32
|
local count = redis.call('INCRBYFLOAT', KEYS[1], cost)
|
|
34
|
-
|
|
33
|
+
local ttl_after = redis.call('TTL', KEYS[1])
|
|
34
|
+
if ttl_after < 0 then
|
|
35
35
|
redis.call('EXPIRE', KEYS[1], ttl)
|
|
36
|
+
ttl_after = tonumber(ttl)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
return {count,
|
|
39
|
+
return {count, ttl_after}
|
|
39
40
|
LUA
|
|
40
41
|
|
|
41
42
|
# Atomic SADD + SCARD + conditional EXPIRE. SET-cardinality counterpart
|
|
42
|
-
# of INCR_SCRIPT; same shape (read TTL,
|
|
43
|
+
# of INCR_SCRIPT; same shape (mutate, read TTL, set TTL when missing,
|
|
43
44
|
# return post-state {count, TTL}). count is SCARD, not the SADD return.
|
|
44
45
|
#
|
|
45
|
-
#
|
|
46
|
+
# ttl_after < 0 covers TTL=-2 (key missing) and TTL=-1 (no expiry),
|
|
46
47
|
# so this also self-heals orphan keys left without TTL.
|
|
47
48
|
SADD_SCRIPT = Labkit::Redis::Script.new(<<~LUA)
|
|
48
49
|
local ttl = ARGV[1]
|
|
49
50
|
local member = ARGV[2]
|
|
50
|
-
local ttl_before = redis.call('TTL', KEYS[1])
|
|
51
51
|
|
|
52
52
|
redis.call('SADD', KEYS[1], member)
|
|
53
53
|
local count = redis.call('SCARD', KEYS[1])
|
|
54
|
-
|
|
54
|
+
local ttl_after = redis.call('TTL', KEYS[1])
|
|
55
|
+
if ttl_after < 0 then
|
|
55
56
|
redis.call('EXPIRE', KEYS[1], ttl)
|
|
57
|
+
ttl_after = tonumber(ttl)
|
|
56
58
|
end
|
|
57
59
|
|
|
58
|
-
return {count,
|
|
60
|
+
return {count, ttl_after}
|
|
59
61
|
LUA
|
|
60
62
|
|
|
61
63
|
def initialize(name:, rules:, redis:, logger:)
|
|
@@ -247,7 +249,7 @@ module Labkit
|
|
|
247
249
|
end
|
|
248
250
|
|
|
249
251
|
# Atomically increments the counter by `cost`, sets the TTL on first
|
|
250
|
-
# write, and
|
|
252
|
+
# write, and returns the window's remaining TTL, all in one Redis
|
|
251
253
|
# operation via Lua. See INCR_SCRIPT for the script body.
|
|
252
254
|
#
|
|
253
255
|
# count is parsed as Float because INCRBYFLOAT returns a string-encoded
|