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 +4 -4
- data/lib/labkit/rate_limit/evaluator.rb +18 -10
- data/lib/labkit/rate_limit/result.rb +34 -16
- 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: 154a169d5b47821eb411551dd452755270a671c334b9f041b7c20090b8d35074
|
|
4
|
+
data.tar.gz: 5373cb3360f29eb1fe9facb99b866423a26785b61c9bc8390f6b4623dba6bff5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
97
|
-
|
|
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?
|
|
7
|
-
# exceeded?
|
|
8
|
-
# action
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# rule
|
|
15
|
-
# error?
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
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
|