gitlab-labkit 4.3.0 → 4.4.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/.gitlab/CODEOWNERS +1 -1
- data/CODEOWNERS +1 -1
- data/lib/labkit/rate_limit/README.md +7 -0
- data/lib/labkit/rate_limit/matcher.rb +35 -12
- 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: 1e7ab6ac58c584a113159e7f42678f508078c83cdeb9a21c86a7f60a5c936b21
|
|
4
|
+
data.tar.gz: b4c3abaa02eafb546284dc68a08ba4443da4c1b13aa824c98f51e0dac3e749da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b032aa2876124669bf1eac1bc09cf67e79b4df4105e95fbc7d416132dd8d5bf1088c49e4c88b304e9a796e119edea60e4c2fae49a70aba050889a928b66b206
|
|
7
|
+
data.tar.gz: 91066213316a173023257e30507fdb8c7c0e4e0b360b91dd1efcafebbacd291c82f62e9470798aa86488ae991bb8f81b1c395856ee2e03b537e3ae5a244d78c3
|
data/.gitlab/CODEOWNERS
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael
|
|
1
|
+
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael @sankalp_gl @hardikgala @nindurkar @ashs2
|
data/CODEOWNERS
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# CODEOWNERS is used to lookup assignees for
|
|
2
2
|
# Renovate Bot dependency change Merge Requests.
|
|
3
3
|
# https://docs.renovatebot.com/configuration-options/#assigneesfromcodeowners
|
|
4
|
-
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael @e_forbes @M_Alvarez @mwoolf
|
|
4
|
+
* @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael @e_forbes @M_Alvarez @mwoolf @sankalp_gl @hardikgala @nindurkar @ashs2
|
|
@@ -174,10 +174,17 @@ A `match` hash gates whether a rule applies. Each value is normalised through
|
|
|
174
174
|
| `Regexp` | `re` | `match: { endpoint: %r{\A/api/} }` |
|
|
175
175
|
| `{ eq: <value> }` | `eq` | `match: { method: { eq: "POST" } }` (YAML-friendly) |
|
|
176
176
|
| `{ re: <source> }`| `re` | `match: { endpoint: { re: '\A/api/' } }` (YAML-friendly) |
|
|
177
|
+
| `{ oneOf: <Array> }` | `oneOf` | `match: { user: { oneOf: ["7", "42"] } }` (YAML-friendly) |
|
|
177
178
|
|
|
178
179
|
`re` coerces the identifier value via `#to_s` before matching, so it can be
|
|
179
180
|
used against non-String values (e.g. matching a 503 status against `{ re: '^5' }`).
|
|
180
181
|
|
|
182
|
+
`oneOf` is set membership via `Set#include?` (`eql?`/`hash` equality, no
|
|
183
|
+
coercion): it agrees with `eq` for Strings, Symbols, booleans, nil, and
|
|
184
|
+
same-class numerics, but cross-type numerics differ (`1 == 1.0`, yet
|
|
185
|
+
`{ oneOf: [1] }` does not match `1.0`). A bare Array is rejected; membership must
|
|
186
|
+
be spelled `{ oneOf: [...] }` explicitly.
|
|
187
|
+
|
|
181
188
|
Glob and prefix matchers are intentionally out of scope.
|
|
182
189
|
|
|
183
190
|
### Evaluation flow
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Labkit
|
|
4
6
|
module RateLimit
|
|
5
7
|
# Matcher is the internal representation of a single key/value predicate in
|
|
@@ -11,6 +13,14 @@ module Labkit
|
|
|
11
13
|
# - a Regexp instance -> :re matcher (Ruby convenience)
|
|
12
14
|
# - { eq: <value> } -> :eq matcher (canonical, YAML-compatible)
|
|
13
15
|
# - { re: <String|Regexp> } -> :re matcher (canonical, YAML-compatible)
|
|
16
|
+
# - { oneOf: <Array> } -> :oneOf matcher (set membership, YAML-compatible)
|
|
17
|
+
#
|
|
18
|
+
# A bare Array is rejected rather than treated as :oneOf, so a value that
|
|
19
|
+
# was meant as a single matcher hash but arrived as an Array fails loudly
|
|
20
|
+
# instead of silently becoming a membership test.
|
|
21
|
+
#
|
|
22
|
+
# :oneOf membership is Set#include? (eql?/hash equality), not :eq's ==, so
|
|
23
|
+
# cross-type numerics differ (1 == 1.0, but {oneOf: [1]} does not match 1.0).
|
|
14
24
|
#
|
|
15
25
|
# Hash-key naming follows the metrics-catalog selector pattern. Glob,
|
|
16
26
|
# prefix, and other matcher kinds are intentionally out of scope here; see
|
|
@@ -20,7 +30,7 @@ module Labkit
|
|
|
20
30
|
# the regex, so callers can match non-String identifier values such as
|
|
21
31
|
# Integer status codes (e.g. {status: { re: "^5" }} against status: 503).
|
|
22
32
|
class Matcher < Data.define(:type, :value)
|
|
23
|
-
KNOWN_HASH_KEYS = %i[eq re].freeze
|
|
33
|
+
KNOWN_HASH_KEYS = %i[eq re oneOf].freeze
|
|
24
34
|
MAX_REGEX_SOURCE_LENGTH = 200
|
|
25
35
|
ERROR_INSPECT_LIMIT = 80
|
|
26
36
|
|
|
@@ -29,11 +39,13 @@ module Labkit
|
|
|
29
39
|
# unbounded elsewhere), and a timeout reaches Evaluator's fail-open
|
|
30
40
|
# rescue, so the request goes unlimited.
|
|
31
41
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
42
|
+
# 50ms is a safety net against catastrophic backtracking, not a
|
|
43
|
+
# performance bound: real matches are far cheaper (median 0.087ms, worst
|
|
44
|
+
# 1.484ms across 6935 paths that timed out in production at 5ms). Those
|
|
45
|
+
# timeouts were wall-clock stalls, since Ruby counts real time and GC
|
|
46
|
+
# pauses on GitLab's git fleet have a p50 of ~72ms. Under 4x CPU
|
|
47
|
+
# oversubscription 50ms still fires on 0.16% of those matches, 5ms 1.33%.
|
|
48
|
+
MATCH_TIMEOUT_SECONDS = 0.05
|
|
37
49
|
|
|
38
50
|
def self.build(input)
|
|
39
51
|
case input
|
|
@@ -42,18 +54,14 @@ module Labkit
|
|
|
42
54
|
when Hash
|
|
43
55
|
from_hash(input)
|
|
44
56
|
when Array
|
|
45
|
-
raise ArgumentError,
|
|
46
|
-
"rate-limit match value must be a single-key Hash like {re: \"...\"} or {eq: ...}, got #{truncate_for_error(input)}"
|
|
57
|
+
raise ArgumentError, invalid_shape_error(input)
|
|
47
58
|
else
|
|
48
59
|
new(type: :eq, value: input)
|
|
49
60
|
end
|
|
50
61
|
end
|
|
51
62
|
|
|
52
63
|
def self.from_hash(input)
|
|
53
|
-
if input.size != 1
|
|
54
|
-
raise ArgumentError,
|
|
55
|
-
"rate-limit match value must be a single-key Hash like {re: \"...\"} or {eq: ...}, got #{truncate_for_error(input)}"
|
|
56
|
-
end
|
|
64
|
+
raise ArgumentError, invalid_shape_error(input) if input.size != 1
|
|
57
65
|
|
|
58
66
|
type, source = input.first
|
|
59
67
|
type_sym = type.to_sym
|
|
@@ -71,6 +79,13 @@ module Labkit
|
|
|
71
79
|
case type_sym
|
|
72
80
|
when :eq
|
|
73
81
|
new(type: :eq, value: source)
|
|
82
|
+
when :oneOf
|
|
83
|
+
unless source.is_a?(Array)
|
|
84
|
+
raise ArgumentError,
|
|
85
|
+
"rate-limit match value {oneOf: ...} must be an Array, got #{truncate_for_error(source)}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
new(type: :oneOf, value: Set.new(source).freeze)
|
|
74
89
|
when :re
|
|
75
90
|
if source.to_s.length > MAX_REGEX_SOURCE_LENGTH
|
|
76
91
|
raise ArgumentError,
|
|
@@ -113,6 +128,12 @@ module Labkit
|
|
|
113
128
|
|
|
114
129
|
private_class_method :with_match_timeout
|
|
115
130
|
|
|
131
|
+
def self.invalid_shape_error(input)
|
|
132
|
+
"rate-limit match value must be a single-key Hash like {re: \"...\"}, {eq: ...} or {oneOf: [...]}, " \
|
|
133
|
+
"got #{truncate_for_error(input)}"
|
|
134
|
+
end
|
|
135
|
+
private_class_method :invalid_shape_error
|
|
136
|
+
|
|
116
137
|
def self.truncate_for_error(value)
|
|
117
138
|
s = value.inspect
|
|
118
139
|
s.length > ERROR_INSPECT_LIMIT ? "#{s[0, ERROR_INSPECT_LIMIT]}...(truncated)" : s
|
|
@@ -123,6 +144,8 @@ module Labkit
|
|
|
123
144
|
case type
|
|
124
145
|
when :eq
|
|
125
146
|
value == identifier_value
|
|
147
|
+
when :oneOf
|
|
148
|
+
value.include?(identifier_value)
|
|
126
149
|
when :re
|
|
127
150
|
value.match?(identifier_value.to_s)
|
|
128
151
|
else
|