gitlab-labkit 2.0.0 → 2.2.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/.copier-answers.yml +2 -1
- data/.gitlab-ci-other-versions.yml +5 -0
- data/.gitlab-ci.yml +1 -1
- data/docker-compose.yml +1 -1
- data/gitlab-labkit.gemspec +7 -7
- data/lib/labkit/fips.rb +2 -2
- data/lib/labkit/rate_limit/evaluator.rb +121 -21
- data/lib/labkit/rate_limit/limiter.rb +13 -4
- data/lib/labkit/rate_limit/rule.rb +26 -3
- data/lib/labkit/rate_limit.rb +5 -2
- metadata +44 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e99765f42b600bc7e6307b75665409c3e3ee3b6aabf69f5bdb14d65fe20d8cdc
|
|
4
|
+
data.tar.gz: b2623750accd4178e7d938ebf87abbb6e1d40f14d4e252b9654f015d4141ce3c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfc0d196a34588965f17b6598b7bf8b1cdaa8d90d79e62bf5a4d75e07537473ee4a8011c7718e18af64d4ec216a85b7bd3a3099523404f6bd7c892b5a7b4ae35
|
|
7
|
+
data.tar.gz: fdb892b50ca446d76d999e7397c57df1539f6724e31ea698b440a7d08a5544e25246d2a4d7ed5bb4516dc5592b31fc7bb1860d29baac43bdc3b94052d011c24d
|
data/.copier-answers.yml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# See the project for instructions on how to update the project
|
|
4
4
|
#
|
|
5
5
|
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
6
|
-
_commit: v1.
|
|
6
|
+
_commit: v1.50.0
|
|
7
7
|
_src_path: https://gitlab.com/gitlab-com/gl-infra/common-template-copier.git
|
|
8
8
|
ee_licensed: false
|
|
9
9
|
gitlab_namespace: gitlab-org/ruby/gems
|
|
@@ -11,6 +11,7 @@ golang: false
|
|
|
11
11
|
helm: false
|
|
12
12
|
initial_codeowners: '@reprazent @andrewn @mkaeppler @ayufan'
|
|
13
13
|
jsonnet: false
|
|
14
|
+
measure_performance: false
|
|
14
15
|
project_name: labkit-ruby
|
|
15
16
|
release_platform: false
|
|
16
17
|
ruby: true
|
data/.gitlab-ci.yml
CHANGED
data/docker-compose.yml
CHANGED
data/gitlab-labkit.gemspec
CHANGED
|
@@ -22,17 +22,17 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
spec.required_ruby_version = ">= 3.3", "< 5"
|
|
23
23
|
|
|
24
24
|
# Please maintain alphabetical order for dependencies
|
|
25
|
-
spec.add_runtime_dependency "actionpack", ">= 5.0.0", "< 8.
|
|
26
|
-
spec.add_runtime_dependency "activesupport", ">= 5.0.0", "< 8.
|
|
25
|
+
spec.add_runtime_dependency "actionpack", ">= 5.0.0", "< 8.2.0"
|
|
26
|
+
spec.add_runtime_dependency "activesupport", ">= 5.0.0", "< 8.2.0"
|
|
27
27
|
spec.add_runtime_dependency "grpc", ">= 1.75" # Be sure to update the "grpc-tools" dev_dependency too
|
|
28
28
|
spec.add_runtime_dependency "google-protobuf", ">= 3.25", "< 5.0"
|
|
29
29
|
spec.add_runtime_dependency "jaeger-client", "~> 1.1.0"
|
|
30
30
|
spec.add_runtime_dependency "json_schemer", ">= 2.3.0", "< 3.0"
|
|
31
31
|
spec.add_runtime_dependency "openssl", "~> 3.3.2"
|
|
32
|
-
spec.add_runtime_dependency "opentelemetry-sdk", "
|
|
33
|
-
spec.add_runtime_dependency "opentelemetry-instrumentation-all", "
|
|
34
|
-
spec.add_runtime_dependency "opentelemetry-exporter-otlp", "
|
|
35
|
-
spec.add_runtime_dependency "opentracing", "
|
|
32
|
+
spec.add_runtime_dependency "opentelemetry-sdk", ">= 1.10", "< 2"
|
|
33
|
+
spec.add_runtime_dependency "opentelemetry-instrumentation-all", ">= 0.89", "< 1"
|
|
34
|
+
spec.add_runtime_dependency "opentelemetry-exporter-otlp", ">= 0.31", "< 1"
|
|
35
|
+
spec.add_runtime_dependency "opentracing", ">= 0.4", "< 1"
|
|
36
36
|
spec.add_runtime_dependency "pg_query", ">= 6.1.0", "< 7.0"
|
|
37
37
|
spec.add_runtime_dependency "prometheus-client-mmap", ">= 1.2", "< 2.0"
|
|
38
38
|
spec.add_runtime_dependency "redis", "> 3.0.0", "< 6.0.0"
|
|
@@ -49,7 +49,7 @@ Gem::Specification.new do |spec|
|
|
|
49
49
|
spec.add_development_dependency "pry", "~> 0.12"
|
|
50
50
|
spec.add_development_dependency "pry-byebug", "~> 3.11"
|
|
51
51
|
spec.add_development_dependency "rack", "~> 2.0"
|
|
52
|
-
spec.add_development_dependency "railties", ">= 5.0.0", "< 8.
|
|
52
|
+
spec.add_development_dependency "railties", ">= 5.0.0", "< 8.2.0"
|
|
53
53
|
spec.add_development_dependency "rake", "~> 13.2"
|
|
54
54
|
spec.add_development_dependency "rest-client", "~> 2.1.0"
|
|
55
55
|
spec.add_development_dependency "rspec", "~> 3.12.0"
|
data/lib/labkit/fips.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Labkit
|
|
|
25
25
|
return true if %w[1 true yes].include?(ENV["FIPS_MODE"])
|
|
26
26
|
|
|
27
27
|
# Otherwise, attempt to auto-detect FIPS mode from OpenSSL
|
|
28
|
-
return true if OpenSSL.fips_mode
|
|
28
|
+
return true if ::OpenSSL.fips_mode
|
|
29
29
|
|
|
30
30
|
false
|
|
31
31
|
end
|
|
@@ -44,7 +44,7 @@ module Labkit
|
|
|
44
44
|
|
|
45
45
|
def use_openssl_digest(ruby_algorithm, openssl_algorithm)
|
|
46
46
|
::Digest.send(:remove_const, ruby_algorithm) # rubocop:disable GitlabSecurity/PublicSend
|
|
47
|
-
::Digest.const_set(ruby_algorithm, OpenSSL::Digest.const_get(openssl_algorithm, false))
|
|
47
|
+
::Digest.const_set(ruby_algorithm, ::OpenSSL::Digest.const_get(openssl_algorithm, false))
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -38,6 +38,26 @@ module Labkit
|
|
|
38
38
|
return {count, redis.call('TTL', KEYS[1])}
|
|
39
39
|
LUA
|
|
40
40
|
|
|
41
|
+
# Atomic SADD + SCARD + conditional EXPIRE. SET-cardinality counterpart
|
|
42
|
+
# of INCR_SCRIPT; same shape (read TTL, mutate, set TTL when missing,
|
|
43
|
+
# return post-state {count, TTL}). count is SCARD, not the SADD return.
|
|
44
|
+
#
|
|
45
|
+
# ttl_before < 0 covers TTL=-2 (key missing) and TTL=-1 (no expiry),
|
|
46
|
+
# so this also self-heals orphan keys left without TTL.
|
|
47
|
+
SADD_SCRIPT = Labkit::Redis::Script.new(<<~LUA)
|
|
48
|
+
local ttl = ARGV[1]
|
|
49
|
+
local member = ARGV[2]
|
|
50
|
+
local ttl_before = redis.call('TTL', KEYS[1])
|
|
51
|
+
|
|
52
|
+
redis.call('SADD', KEYS[1], member)
|
|
53
|
+
local count = redis.call('SCARD', KEYS[1])
|
|
54
|
+
if ttl_before < 0 then
|
|
55
|
+
redis.call('EXPIRE', KEYS[1], ttl)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return {count, redis.call('TTL', KEYS[1])}
|
|
59
|
+
LUA
|
|
60
|
+
|
|
41
61
|
def initialize(name:, rules:, redis:, logger:)
|
|
42
62
|
@name = name
|
|
43
63
|
@rules = rules
|
|
@@ -45,8 +65,8 @@ module Labkit
|
|
|
45
65
|
@logger = logger
|
|
46
66
|
end
|
|
47
67
|
|
|
48
|
-
def check(identifier, cost: 1)
|
|
49
|
-
check_rules(identifier, cost)
|
|
68
|
+
def check(identifier, cost: 1, rule_context: nil)
|
|
69
|
+
check_rules(identifier, cost, rule_context)
|
|
50
70
|
rescue StandardError => e
|
|
51
71
|
# Intentionally broad: fail-open applies to any unexpected error (network,
|
|
52
72
|
# timeout, OOM) not only Redis protocol errors.
|
|
@@ -58,8 +78,8 @@ module Labkit
|
|
|
58
78
|
# Read-without-increment counterpart to {#check}. Same matching and Result
|
|
59
79
|
# shape; the underlying Redis counter is not mutated and the TTL is not
|
|
60
80
|
# extended. A missing Redis key is treated as count=0 (matched, not exceeded).
|
|
61
|
-
def peek(identifier)
|
|
62
|
-
peek_rules(identifier)
|
|
81
|
+
def peek(identifier, rule_context: nil)
|
|
82
|
+
peek_rules(identifier, rule_context)
|
|
63
83
|
rescue StandardError => e
|
|
64
84
|
report_error_metrics
|
|
65
85
|
log_error(e, identifier)
|
|
@@ -70,11 +90,22 @@ module Labkit
|
|
|
70
90
|
|
|
71
91
|
# :log rules are non-terminating: they emit metrics and continue,
|
|
72
92
|
# so a shadow :log rule cannot disable a following :block rule.
|
|
73
|
-
|
|
93
|
+
#
|
|
94
|
+
# SET-mode rules (rule.count_distinct set) that match but whose identifier
|
|
95
|
+
# is missing the count_distinct key fail open + log + bump errors_total, and
|
|
96
|
+
# the loop continues to the next rule (the rule is treated as not applicable
|
|
97
|
+
# rather than aborting the whole evaluation).
|
|
98
|
+
def check_rules(identifier, cost, rule_context)
|
|
74
99
|
@rules.each do |rule|
|
|
75
100
|
next unless rule_matches?(rule, identifier)
|
|
76
101
|
|
|
77
|
-
|
|
102
|
+
if rule.count_distinct && missing_count_distinct_value?(rule, identifier)
|
|
103
|
+
log_missing_count_distinct(rule, identifier)
|
|
104
|
+
report_error_metrics
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
result = evaluate_rule(rule, identifier, cost, rule_context)
|
|
78
109
|
report_matched_metrics(result)
|
|
79
110
|
return result unless rule.action == :log
|
|
80
111
|
end
|
|
@@ -85,12 +116,16 @@ module Labkit
|
|
|
85
116
|
|
|
86
117
|
# Mirror of check_rules without metrics: peek skips :log rules (their state
|
|
87
118
|
# is unobservable through peek).
|
|
88
|
-
|
|
119
|
+
#
|
|
120
|
+
# peek does not need the count_distinct identifier key - it reads SCARD on
|
|
121
|
+
# the rule-keyed compound key, which contains the cardinality across all
|
|
122
|
+
# members. So missing-key fail-open does not apply here.
|
|
123
|
+
def peek_rules(identifier, rule_context)
|
|
89
124
|
@rules.each do |rule|
|
|
90
125
|
next if rule.action == :log
|
|
91
126
|
next unless rule_matches?(rule, identifier)
|
|
92
127
|
|
|
93
|
-
return peek_rule(rule, identifier)
|
|
128
|
+
return peek_rule(rule, identifier, rule_context)
|
|
94
129
|
end
|
|
95
130
|
|
|
96
131
|
Result.new(matched: false, action: :allow)
|
|
@@ -100,21 +135,34 @@ module Labkit
|
|
|
100
135
|
rule.match.all? { |key, matcher| matcher.match?(identifier[key]) }
|
|
101
136
|
end
|
|
102
137
|
|
|
103
|
-
def
|
|
138
|
+
def missing_count_distinct_value?(rule, identifier)
|
|
139
|
+
value = identifier[rule.count_distinct]
|
|
140
|
+
value.nil? || value.to_s.empty?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def evaluate_rule(rule, identifier, cost, rule_context)
|
|
104
144
|
redis_key = build_redis_key(rule, identifier)
|
|
105
|
-
resolved_limit = Integer(resolve_value(rule.limit))
|
|
106
|
-
resolved_period = Integer(resolve_value(rule.period))
|
|
145
|
+
resolved_limit = Integer(resolve_value(rule.limit, rule_context))
|
|
146
|
+
resolved_period = Integer(resolve_value(rule.period, rule_context))
|
|
147
|
+
|
|
148
|
+
# cost is ignored for count_distinct rules: SADD is binary (a member is
|
|
149
|
+
# either added or not), and the post-add count is SCARD regardless.
|
|
150
|
+
count, ttl =
|
|
151
|
+
if rule.count_distinct
|
|
152
|
+
sadd_with_ttl(redis_key, identifier[rule.count_distinct], resolved_period)
|
|
153
|
+
else
|
|
154
|
+
incr_with_ttl(redis_key, resolved_period, cost)
|
|
155
|
+
end
|
|
107
156
|
|
|
108
|
-
count, ttl = incr_with_ttl(redis_key, resolved_period, cost)
|
|
109
157
|
build_result(rule, resolved_limit, resolved_period, count, ttl)
|
|
110
158
|
end
|
|
111
159
|
|
|
112
|
-
def peek_rule(rule, identifier)
|
|
160
|
+
def peek_rule(rule, identifier, rule_context)
|
|
113
161
|
redis_key = build_redis_key(rule, identifier)
|
|
114
|
-
resolved_limit = Integer(resolve_value(rule.limit))
|
|
115
|
-
resolved_period = Integer(resolve_value(rule.period))
|
|
162
|
+
resolved_limit = Integer(resolve_value(rule.limit, rule_context))
|
|
163
|
+
resolved_period = Integer(resolve_value(rule.period, rule_context))
|
|
116
164
|
|
|
117
|
-
count, ttl = read_with_ttl(redis_key)
|
|
165
|
+
count, ttl = rule.count_distinct ? scard_with_ttl(redis_key) : read_with_ttl(redis_key)
|
|
118
166
|
build_result(rule, resolved_limit, resolved_period, count, ttl)
|
|
119
167
|
end
|
|
120
168
|
|
|
@@ -147,8 +195,29 @@ module Labkit
|
|
|
147
195
|
value.to_s
|
|
148
196
|
end
|
|
149
197
|
|
|
150
|
-
|
|
151
|
-
|
|
198
|
+
# Resolve a limit/period value. Plain values pass through; callables are
|
|
199
|
+
# invoked according to their arity:
|
|
200
|
+
#
|
|
201
|
+
# - Zero-arity callables call with no args (e.g. -> { ApplicationSetting.current.foo }).
|
|
202
|
+
# - Callables with arity >= 1 receive +rule_context+, which may be nil
|
|
203
|
+
# if the caller didn't pass it. They must therefore handle nil -
|
|
204
|
+
# typically with `ctx&.[](:key) || default`.
|
|
205
|
+
# - Variadic callables (negative arity, e.g. ->(*args) { ... }) take
|
|
206
|
+
# the zero-arg path. Opt into rule_context by writing the lambda
|
|
207
|
+
# with exactly one required parameter: ->(ctx) { ... }. This avoids
|
|
208
|
+
# the footgun where ->(*args) silently receives [rule_context] and
|
|
209
|
+
# the caller's overrides never take effect.
|
|
210
|
+
# - Callables that respond to +call+ but not +arity+ (e.g. a class with
|
|
211
|
+
# `def call` and no explicit arity) take the zero-arg path, preserving
|
|
212
|
+
# the pre-rule_context behaviour for custom callable objects.
|
|
213
|
+
#
|
|
214
|
+
# This lets rules carry callables that depend on per-request context (e.g.
|
|
215
|
+
# per-namespace settings) without rebuilding the Rule on every call or
|
|
216
|
+
# smuggling state through globals.
|
|
217
|
+
def resolve_value(val, rule_context = nil)
|
|
218
|
+
return val unless val.respond_to?(:call)
|
|
219
|
+
|
|
220
|
+
val.respond_to?(:arity) && val.arity >= 1 ? val.call(rule_context) : val.call
|
|
152
221
|
end
|
|
153
222
|
|
|
154
223
|
def encode_char_value(value)
|
|
@@ -168,7 +237,7 @@ module Labkit
|
|
|
168
237
|
# otherwise.
|
|
169
238
|
def incr_with_ttl(redis_key, period, cost)
|
|
170
239
|
@redis.with do |conn|
|
|
171
|
-
raw_count, ttl =
|
|
240
|
+
raw_count, ttl = INCR_SCRIPT.eval(conn, keys: [redis_key], argv: [period, cost])
|
|
172
241
|
[Float(raw_count), ttl]
|
|
173
242
|
end
|
|
174
243
|
end
|
|
@@ -191,8 +260,29 @@ module Labkit
|
|
|
191
260
|
end
|
|
192
261
|
end
|
|
193
262
|
|
|
194
|
-
|
|
195
|
-
|
|
263
|
+
# Atomic SADD + SCARD + conditional EXPIRE in one Redis operation via Lua.
|
|
264
|
+
# See SADD_SCRIPT for the body. Mirrors incr_with_ttl's shape, including
|
|
265
|
+
# the Float-typed count for uniformity with the INCR path.
|
|
266
|
+
def sadd_with_ttl(redis_key, member, period)
|
|
267
|
+
member_str = encode_char_value(member.to_s)
|
|
268
|
+
@redis.with do |conn|
|
|
269
|
+
raw_count, ttl = SADD_SCRIPT.eval(conn, keys: [redis_key], argv: [period, member_str])
|
|
270
|
+
[Float(raw_count), ttl]
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Pipelined SCARD + TTL. SCARD on a missing key returns 0, so no
|
|
275
|
+
# explicit nil handling is needed (unlike GET in read_with_ttl).
|
|
276
|
+
# SCARD is integer-valued; coerced to Float for type uniformity with
|
|
277
|
+
# the INCR path so callers see a consistent count type.
|
|
278
|
+
def scard_with_ttl(redis_key)
|
|
279
|
+
@redis.with do |conn|
|
|
280
|
+
scard, ttl = conn.pipelined do |pipe|
|
|
281
|
+
pipe.scard(redis_key)
|
|
282
|
+
pipe.ttl(redis_key)
|
|
283
|
+
end
|
|
284
|
+
[Float(scard), ttl]
|
|
285
|
+
end
|
|
196
286
|
end
|
|
197
287
|
|
|
198
288
|
def log_error(error, identifier)
|
|
@@ -204,6 +294,16 @@ module Labkit
|
|
|
204
294
|
)
|
|
205
295
|
end
|
|
206
296
|
|
|
297
|
+
def log_missing_count_distinct(rule, identifier)
|
|
298
|
+
@logger.warn(
|
|
299
|
+
message: "rate_limit_missing_count_distinct",
|
|
300
|
+
name: @name,
|
|
301
|
+
rule: rule.name,
|
|
302
|
+
count_distinct: rule.count_distinct.to_s,
|
|
303
|
+
identifier: identifier&.to_h
|
|
304
|
+
)
|
|
305
|
+
end
|
|
306
|
+
|
|
207
307
|
def report_matched_metrics(result)
|
|
208
308
|
Metrics.calls_total.increment(
|
|
209
309
|
rate_limiter: @name,
|
|
@@ -35,10 +35,18 @@ module Labkit
|
|
|
35
35
|
# @param cost [Numeric] amount to add to the counter. Defaults to 1
|
|
36
36
|
# (count-mode). Pass a non-1 Numeric for cost-mode counters such as
|
|
37
37
|
# resource-usage limits; passing 0 reads the counter without writing.
|
|
38
|
+
# @param rule_context [Hash, nil] optional per-request context passed to
|
|
39
|
+
# one-arity callables on +limit+/+period+. Lets rules resolve dynamic
|
|
40
|
+
# configuration (e.g. per-namespace settings) without rebuilding the
|
|
41
|
+
# Rule or doing out-of-band DB queries. Zero-arity callables ignore it.
|
|
42
|
+
# The key contract is owned by the rule's callable, not validated
|
|
43
|
+
# here: if the rule reads ctx[:limit] and the caller passes
|
|
44
|
+
# ctx[:lmit], the callable's fallback branch fires silently. Keep
|
|
45
|
+
# the rule definition and the call site colocated.
|
|
38
46
|
# @return [Result]
|
|
39
|
-
def check(identifier, cost: 1)
|
|
47
|
+
def check(identifier, cost: 1, rule_context: nil)
|
|
40
48
|
id = identifier.is_a?(Identifier) ? identifier : Identifier.new(identifier)
|
|
41
|
-
@evaluator.check(id, cost: cost)
|
|
49
|
+
@evaluator.check(id, cost: cost, rule_context: rule_context)
|
|
42
50
|
end
|
|
43
51
|
|
|
44
52
|
# Read the current rate-limit state without incrementing the counter.
|
|
@@ -54,10 +62,11 @@ module Labkit
|
|
|
54
62
|
# open identically to {#check}.
|
|
55
63
|
#
|
|
56
64
|
# @param identifier [Identifier, Hash] caller attributes for this request
|
|
65
|
+
# @param rule_context [Hash, nil] see {#check}
|
|
57
66
|
# @return [Result]
|
|
58
|
-
def peek(identifier)
|
|
67
|
+
def peek(identifier, rule_context: nil)
|
|
59
68
|
id = identifier.is_a?(Identifier) ? identifier : Identifier.new(identifier)
|
|
60
|
-
@evaluator.peek(id)
|
|
69
|
+
@evaluator.peek(id, rule_context: rule_context)
|
|
61
70
|
end
|
|
62
71
|
|
|
63
72
|
private
|
|
@@ -17,12 +17,32 @@ module Labkit
|
|
|
17
17
|
# (count but always permit; terminates evaluation on match
|
|
18
18
|
# regardless of whether the limit was exceeded)
|
|
19
19
|
# characteristics - identifier keys used to build the compound Redis counter key
|
|
20
|
+
# count_distinct - optional Symbol naming an identifier key. When set, the rule
|
|
21
|
+
# counts the number of distinct values seen for that key within
|
|
22
|
+
# the (characteristics-bucketed) period, backed by a Redis SET.
|
|
23
|
+
# When nil (default), the rule counts the number of calls,
|
|
24
|
+
# backed by INCR. The named key must not overlap +characteristics+.
|
|
20
25
|
#
|
|
21
26
|
# +name+ must be a lowercase alphanumeric-and-underscore string of at most 64
|
|
22
27
|
# characters. It is used as the middle segment of every Redis counter key for
|
|
23
28
|
# this rule, so changing a rule's name mid-window abandons its in-flight counters.
|
|
24
|
-
Rule = Data.define(:name, :match, :limit, :period, :action, :characteristics) do
|
|
25
|
-
def
|
|
29
|
+
Rule = Data.define(:name, :match, :limit, :period, :action, :characteristics, :count_distinct) do
|
|
30
|
+
def self.normalize_count_distinct(value, characteristics_arr)
|
|
31
|
+
sym =
|
|
32
|
+
case value
|
|
33
|
+
when nil then nil
|
|
34
|
+
when Symbol then value
|
|
35
|
+
when String then value.to_sym
|
|
36
|
+
else
|
|
37
|
+
raise ArgumentError, "count_distinct must be a Symbol, String, or nil, got #{value.class}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
raise ArgumentError, "count_distinct #{sym.inspect} must not overlap characteristics #{characteristics_arr.inspect}" if sym && characteristics_arr.include?(sym)
|
|
41
|
+
|
|
42
|
+
sym
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(name:, limit:, period:, characteristics:, match: {}, action: :block, count_distinct: nil)
|
|
26
46
|
raise ArgumentError, "name must be a String or Symbol, got #{name.class}" unless name.is_a?(String) || name.is_a?(Symbol)
|
|
27
47
|
|
|
28
48
|
name_str = name.to_s
|
|
@@ -36,13 +56,16 @@ module Labkit
|
|
|
36
56
|
raise ArgumentError, "Rule name too long: #{name.inspect}. Maximum 64 characters" if name_str.length > RULE_NAME_MAX_LENGTH
|
|
37
57
|
end
|
|
38
58
|
|
|
59
|
+
characteristics_arr = Array(characteristics).map(&:to_sym).freeze
|
|
60
|
+
|
|
39
61
|
super(
|
|
40
62
|
name: name_str.freeze,
|
|
41
63
|
match: match.transform_keys(&:to_sym).transform_values { |v| Matcher.build(v) }.freeze,
|
|
42
64
|
limit: limit,
|
|
43
65
|
period: period,
|
|
44
66
|
action: action_sym,
|
|
45
|
-
characteristics:
|
|
67
|
+
characteristics: characteristics_arr,
|
|
68
|
+
count_distinct: self.class.normalize_count_distinct(count_distinct, characteristics_arr)
|
|
46
69
|
)
|
|
47
70
|
end
|
|
48
71
|
end
|
data/lib/labkit/rate_limit.rb
CHANGED
|
@@ -44,9 +44,12 @@ module Labkit
|
|
|
44
44
|
# @param redis [Object, nil] Redis client; falls back to config.redis
|
|
45
45
|
# @param logger [Logger, nil] logger; falls back to config.logger
|
|
46
46
|
# @param cost [Numeric] amount to add to the counter; see Limiter#check
|
|
47
|
+
# @param rule_context [Hash, nil] per-request context for one-arity
|
|
48
|
+
# callables on rule limit/period; see Limiter#check
|
|
47
49
|
# @return [Result]
|
|
48
|
-
def check(name:, identifier:, rules:, redis: nil, logger: nil, cost: 1)
|
|
49
|
-
Limiter.new(name: name, rules: rules, redis: redis, logger: logger)
|
|
50
|
+
def check(name:, identifier:, rules:, redis: nil, logger: nil, cost: 1, rule_context: nil)
|
|
51
|
+
Limiter.new(name: name, rules: rules, redis: redis, logger: logger)
|
|
52
|
+
.check(identifier, cost: cost, rule_context: rule_context)
|
|
50
53
|
end
|
|
51
54
|
end
|
|
52
55
|
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: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Newdigate
|
|
@@ -18,7 +18,7 @@ dependencies:
|
|
|
18
18
|
version: 5.0.0
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: 8.
|
|
21
|
+
version: 8.2.0
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -28,7 +28,7 @@ dependencies:
|
|
|
28
28
|
version: 5.0.0
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: 8.
|
|
31
|
+
version: 8.2.0
|
|
32
32
|
- !ruby/object:Gem::Dependency
|
|
33
33
|
name: activesupport
|
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,7 +38,7 @@ dependencies:
|
|
|
38
38
|
version: 5.0.0
|
|
39
39
|
- - "<"
|
|
40
40
|
- !ruby/object:Gem::Version
|
|
41
|
-
version: 8.
|
|
41
|
+
version: 8.2.0
|
|
42
42
|
type: :runtime
|
|
43
43
|
prerelease: false
|
|
44
44
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -48,7 +48,7 @@ dependencies:
|
|
|
48
48
|
version: 5.0.0
|
|
49
49
|
- - "<"
|
|
50
50
|
- !ruby/object:Gem::Version
|
|
51
|
-
version: 8.
|
|
51
|
+
version: 8.2.0
|
|
52
52
|
- !ruby/object:Gem::Dependency
|
|
53
53
|
name: grpc
|
|
54
54
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -135,58 +135,82 @@ dependencies:
|
|
|
135
135
|
name: opentelemetry-sdk
|
|
136
136
|
requirement: !ruby/object:Gem::Requirement
|
|
137
137
|
requirements:
|
|
138
|
-
- - "
|
|
138
|
+
- - ">="
|
|
139
139
|
- !ruby/object:Gem::Version
|
|
140
140
|
version: '1.10'
|
|
141
|
+
- - "<"
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '2'
|
|
141
144
|
type: :runtime
|
|
142
145
|
prerelease: false
|
|
143
146
|
version_requirements: !ruby/object:Gem::Requirement
|
|
144
147
|
requirements:
|
|
145
|
-
- - "
|
|
148
|
+
- - ">="
|
|
146
149
|
- !ruby/object:Gem::Version
|
|
147
150
|
version: '1.10'
|
|
151
|
+
- - "<"
|
|
152
|
+
- !ruby/object:Gem::Version
|
|
153
|
+
version: '2'
|
|
148
154
|
- !ruby/object:Gem::Dependency
|
|
149
155
|
name: opentelemetry-instrumentation-all
|
|
150
156
|
requirement: !ruby/object:Gem::Requirement
|
|
151
157
|
requirements:
|
|
152
|
-
- - "
|
|
158
|
+
- - ">="
|
|
159
|
+
- !ruby/object:Gem::Version
|
|
160
|
+
version: '0.89'
|
|
161
|
+
- - "<"
|
|
153
162
|
- !ruby/object:Gem::Version
|
|
154
|
-
version:
|
|
163
|
+
version: '1'
|
|
155
164
|
type: :runtime
|
|
156
165
|
prerelease: false
|
|
157
166
|
version_requirements: !ruby/object:Gem::Requirement
|
|
158
167
|
requirements:
|
|
159
|
-
- - "
|
|
168
|
+
- - ">="
|
|
169
|
+
- !ruby/object:Gem::Version
|
|
170
|
+
version: '0.89'
|
|
171
|
+
- - "<"
|
|
160
172
|
- !ruby/object:Gem::Version
|
|
161
|
-
version:
|
|
173
|
+
version: '1'
|
|
162
174
|
- !ruby/object:Gem::Dependency
|
|
163
175
|
name: opentelemetry-exporter-otlp
|
|
164
176
|
requirement: !ruby/object:Gem::Requirement
|
|
165
177
|
requirements:
|
|
166
|
-
- - "
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0.31'
|
|
181
|
+
- - "<"
|
|
167
182
|
- !ruby/object:Gem::Version
|
|
168
|
-
version:
|
|
183
|
+
version: '1'
|
|
169
184
|
type: :runtime
|
|
170
185
|
prerelease: false
|
|
171
186
|
version_requirements: !ruby/object:Gem::Requirement
|
|
172
187
|
requirements:
|
|
173
|
-
- - "
|
|
188
|
+
- - ">="
|
|
189
|
+
- !ruby/object:Gem::Version
|
|
190
|
+
version: '0.31'
|
|
191
|
+
- - "<"
|
|
174
192
|
- !ruby/object:Gem::Version
|
|
175
|
-
version:
|
|
193
|
+
version: '1'
|
|
176
194
|
- !ruby/object:Gem::Dependency
|
|
177
195
|
name: opentracing
|
|
178
196
|
requirement: !ruby/object:Gem::Requirement
|
|
179
197
|
requirements:
|
|
180
|
-
- - "
|
|
198
|
+
- - ">="
|
|
181
199
|
- !ruby/object:Gem::Version
|
|
182
200
|
version: '0.4'
|
|
201
|
+
- - "<"
|
|
202
|
+
- !ruby/object:Gem::Version
|
|
203
|
+
version: '1'
|
|
183
204
|
type: :runtime
|
|
184
205
|
prerelease: false
|
|
185
206
|
version_requirements: !ruby/object:Gem::Requirement
|
|
186
207
|
requirements:
|
|
187
|
-
- - "
|
|
208
|
+
- - ">="
|
|
188
209
|
- !ruby/object:Gem::Version
|
|
189
210
|
version: '0.4'
|
|
211
|
+
- - "<"
|
|
212
|
+
- !ruby/object:Gem::Version
|
|
213
|
+
version: '1'
|
|
190
214
|
- !ruby/object:Gem::Dependency
|
|
191
215
|
name: pg_query
|
|
192
216
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -410,7 +434,7 @@ dependencies:
|
|
|
410
434
|
version: 5.0.0
|
|
411
435
|
- - "<"
|
|
412
436
|
- !ruby/object:Gem::Version
|
|
413
|
-
version: 8.
|
|
437
|
+
version: 8.2.0
|
|
414
438
|
type: :development
|
|
415
439
|
prerelease: false
|
|
416
440
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -420,7 +444,7 @@ dependencies:
|
|
|
420
444
|
version: 5.0.0
|
|
421
445
|
- - "<"
|
|
422
446
|
- !ruby/object:Gem::Version
|
|
423
|
-
version: 8.
|
|
447
|
+
version: 8.2.0
|
|
424
448
|
- !ruby/object:Gem::Dependency
|
|
425
449
|
name: rake
|
|
426
450
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -523,6 +547,7 @@ files:
|
|
|
523
547
|
- ".env.example.sh"
|
|
524
548
|
- ".gitignore"
|
|
525
549
|
- ".gitlab-ci-asdf-versions.yml"
|
|
550
|
+
- ".gitlab-ci-other-versions.yml"
|
|
526
551
|
- ".gitlab-ci.yml"
|
|
527
552
|
- ".gitlab/CODEOWNERS"
|
|
528
553
|
- ".gitlab/merge_request_templates/default.md"
|