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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c4b2288dc9341d5acd02a9ad274702ad2c44793c5760f04b6597c7404cad509
4
- data.tar.gz: f6bc276c4452b9952dcd5e194f76d74c787ba122ae39070d35fc507aec06d444
3
+ metadata.gz: 5fbd752b05c1341ef1292c7a3a1ae3555dd27230d7afc830e0c713547bdd02ee
4
+ data.tar.gz: 6dc8eab0332b9b6851627fe83a95bfb0bacefbe3222d712e28a8e210c19fdb8a
5
5
  SHA512:
6
- metadata.gz: fb5d89354c732b5ea0ac8579422a2fd3212e2e79f9d035ad0bdb642258710e2c7cf43abbfbd9c6b6f95d4fe190799ddbbcaf34b0b3d51ff857c0a8330e56af7f
7
- data.tar.gz: cea073f528c655e9df4df9715a6a7a5728f13c55e75190f2ace56ca15aba59ec84ac902037125b54de0f7631fbcd03120fd1a8caad3d8854481d39a73eeb11d6
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 <-->|INCR / TTL / EXPIRE| Redis[(Redis)]
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
- (`count == 1`) and is not extended on subsequent INCRs, so the window is a
241
- true fixed window starting at the first request, not a sliding window.
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: PIPELINE { INCR key, TTL key }
253
- R-->>E: [count, ttl]
254
- alt count == 1 (first write of window)
255
- E->>R: EXPIRE key period
256
- R-->>E: 1
257
- Note over E: ttl returned is -1 here;<br/>build_result falls back to<br/>resolved_period for reset_at.
258
- else count > 1
259
- Note over E: TTL is not extended:<br/>fixed window from first write.
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
- `peek` follows the same shape but uses `GET` instead of `INCR` and never
265
- issues `EXPIRE`. A missing key (`GET nil`, `TTL -2`) is reported as
266
- `count = 0` and the window is treated as not-yet-started.
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
- # ttl_before < 0 covers TTL=-2 (key missing) and TTL=-1 (no expiry).
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
- if ttl_before < 0 then
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, redis.call('TTL', KEYS[1])}
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, mutate, set TTL when missing,
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
- # ttl_before < 0 covers TTL=-2 (key missing) and TTL=-1 (no expiry),
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
- if ttl_before < 0 then
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, redis.call('TTL', KEYS[1])}
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 reads back the post-increment TTL, all in one Redis
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-labkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Newdigate