gitlab-labkit 1.17.0 → 1.18.0

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: 7177b8f9210f24958a14c92f17ad35cc8d4ad7732e04a9407569e865ef2bf364
4
- data.tar.gz: 688c6dd7b7c1a8c1062a1e17c534fa833f86053aff8945ab4d296c2223784711
3
+ metadata.gz: 154a169d5b47821eb411551dd452755270a671c334b9f041b7c20090b8d35074
4
+ data.tar.gz: 5373cb3360f29eb1fe9facb99b866423a26785b61c9bc8390f6b4623dba6bff5
5
5
  SHA512:
6
- metadata.gz: 2b54fe16d5c1cfe12c704a4e84cdea8dece8155dcaa04132c9e52442bec03e6338f6a4bcb065aa87931aa2fedcbd2ecb5ecec390b00d110267ee54e95a5c694c
7
- data.tar.gz: 8abc6a48c3f1b138a84f8502e5c4300bec5f05f98b0c8defb4280d12b1d424f70cf64e0afe3cc344ce61ceae145e9aaf11f252d9611da1e5c6a1f25aa37fe867
6
+ metadata.gz: d8393e9574bdeb229a8089dfdcbe90ed1d68bfd9c7a35a95f654800beb18dab5121b48f82c61d71d1d00a83ba6e60e5c595a020bb8e9237b5a636d019645afd0
7
+ data.tar.gz: 7cf8a3cda6cca43339bf14e814cdd7ee28ebac6de0b4bf21b1c84400eb43bd4061fb0029852fb475293c16a0ae497abc5002556260dd1da680aeb39abc9fbc2c
@@ -53,14 +53,17 @@ module Labkit
53
53
  resolved_limit = Integer(resolve_value(rule.limit))
54
54
  resolved_period = Integer(resolve_value(rule.period))
55
55
 
56
- count = incr_with_ttl(redis_key, resolved_period)
56
+ count, ttl = incr_with_ttl(redis_key, resolved_period)
57
57
  exceeded = count > resolved_limit
58
58
  action = exceeded ? rule.action : :allow
59
-
60
- Result.new(
61
- matched: true, exceeded: exceeded, action: action, rule: rule,
62
- resolved_limit: resolved_limit, resolved_period: resolved_period
59
+ info = Result::Info.new(
60
+ resolved_limit: resolved_limit, resolved_period: resolved_period,
61
+ count: count,
62
+ remaining: [resolved_limit - count, 0].max,
63
+ reset_at: Time.now.utc + (ttl >= 0 ? ttl : resolved_period)
63
64
  )
65
+
66
+ Result.new(matched: true, exceeded: exceeded, action: action, rule: rule, info: info)
64
67
  end
65
68
 
66
69
  def build_redis_key(rule, identifier)
@@ -91,12 +94,17 @@ module Labkit
91
94
  end
92
95
  end
93
96
 
97
+ # Pipelines INCR and TTL so both are fetched in a single round-trip.
98
+ # EXPIRE follows as a separate call only on first write (count == 1).
99
+ # On first write TTL will be -1 (expiry not yet set); callers fall back to period.
94
100
  def incr_with_ttl(redis_key, period)
95
101
  @redis.with do |conn|
96
- count = conn.incr(redis_key)
97
- # Set expiry only on first write to avoid resetting TTL on each call
102
+ count, ttl = conn.pipelined do |pipe|
103
+ pipe.incr(redis_key)
104
+ pipe.ttl(redis_key)
105
+ end
98
106
  conn.expire(redis_key, period) if count == 1
99
- count
107
+ [count, ttl]
100
108
  end
101
109
  end
102
110
 
@@ -117,11 +125,11 @@ module Labkit
117
125
  )
118
126
  Metrics.limit_gauge.set(
119
127
  { rate_limiter: @name, rule: result.rule.name },
120
- result.resolved_limit
128
+ result.info.resolved_limit
121
129
  )
122
130
  Metrics.period_gauge.set(
123
131
  { rate_limiter: @name, rule: result.rule.name },
124
- result.resolved_period
132
+ result.info.resolved_period
125
133
  )
126
134
  end
127
135
 
@@ -3,22 +3,19 @@
3
3
  module Labkit
4
4
  module RateLimit
5
5
  # Result is the return value of Limiter#check.
6
- # matched? - true if a rule's match conditions were satisfied
7
- # exceeded? - true if the matched rule's counter exceeded its limit
8
- # action - the outcome: what the caller should do
9
- # :block = rule matched, exceeded, rule configured to block
10
- # :log = rule matched, exceeded, rule configured to log only
11
- # :allow = rule matched but count within limit, or
12
- # no rule matched, or error (fail-open)
13
- # The rule's configured action is available via rule.action
14
- # rule - the matched Rule object (nil when matched? is false)
15
- # error? - true if Redis was unavailable; result fails open (exceeded? is false)
16
- # resolved_limit - the resolved limit value as Integer (nil when matched? is false or error)
17
- # resolved_period - the resolved period value as Integer (nil when matched? is false or error)
18
- Result = Data.define(:matched, :exceeded, :action, :rule, :error, :resolved_limit, :resolved_period) do
19
- def initialize(
20
- matched:, action:, exceeded: false, rule: nil, error: false,
21
- resolved_limit: nil, resolved_period: nil)
6
+ # matched? - true if a rule's match conditions were satisfied
7
+ # exceeded? - true if the matched rule's counter exceeded its limit
8
+ # action - the outcome: what the caller should do
9
+ # :block = rule matched, exceeded, rule configured to block
10
+ # :log = rule matched, exceeded, rule configured to log only
11
+ # :allow = rule matched but count within limit, or
12
+ # no rule matched, or error (fail-open)
13
+ # The rule's configured action is available via rule.action
14
+ # rule - the matched Rule object (nil when matched? is false)
15
+ # error? - true if Redis was unavailable; result fails open (exceeded? is false)
16
+ # info - Result::Info with per-window counters; nil when matched? is false or error?
17
+ Result = Data.define(:matched, :exceeded, :action, :rule, :error, :info) do
18
+ def initialize(matched:, action: nil, exceeded: false, rule: nil, error: false, info: nil)
22
19
  super
23
20
  end
24
21
 
@@ -33,6 +30,27 @@ module Labkit
33
30
  def error?
34
31
  error
35
32
  end
33
+
34
+ # Returns RFC-compliant rate limit response headers, or {} when no rule matched or an error occurred.
35
+ # Keys: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (Unix timestamp).
36
+ # reset_at is advisory only - derived from a pipelined redis.ttl call, not fully atomic.
37
+ def to_response_headers
38
+ return {} unless matched? && !error? && info
39
+
40
+ {
41
+ "RateLimit-Limit" => info.resolved_limit.to_s,
42
+ "RateLimit-Remaining" => info.remaining.to_s,
43
+ "RateLimit-Reset" => info.reset_at.to_i.to_s
44
+ }
45
+ end
36
46
  end
47
+
48
+ # Per-window counter data attached to a matched Result.
49
+ # resolved_limit - the evaluated limit Integer for this rule
50
+ # resolved_period - the evaluated period Integer (seconds) for this rule
51
+ # count - the raw INCR value; useful for utilization-ratio metrics
52
+ # remaining - requests remaining before the limit is hit (floors at 0)
53
+ # reset_at - best-effort UTC Time when the counter window resets
54
+ Result::Info = Data.define(:resolved_limit, :resolved_period, :count, :remaining, :reset_at)
37
55
  end
38
56
  end
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: 1.17.0
4
+ version: 1.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Newdigate