throttle_machines 0.1.1 → 0.2.4
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/README.md +3 -0
- data/lib/throttle_machines/async_breaker.rb +12 -0
- data/lib/throttle_machines/async_limiter.rb +7 -117
- data/lib/throttle_machines/async_support.rb +75 -0
- data/lib/throttle_machines/controller_helpers.rb +2 -2
- data/lib/throttle_machines/engine.rb +1 -6
- data/lib/throttle_machines/hedged_breaker.rb +2 -2
- data/lib/throttle_machines/hedged_request.rb +2 -2
- data/lib/throttle_machines/instrumentation.rb +19 -19
- data/lib/throttle_machines/middleware.rb +1 -2
- data/lib/throttle_machines/native_speedup.rb +98 -0
- data/lib/throttle_machines/rack_middleware/allow2_ban.rb +11 -22
- data/lib/throttle_machines/rack_middleware/ban_filter.rb +29 -0
- data/lib/throttle_machines/rack_middleware/blocklist.rb +5 -18
- data/lib/throttle_machines/rack_middleware/configuration.rb +14 -14
- data/lib/throttle_machines/rack_middleware/fail2_ban.rb +20 -29
- data/lib/throttle_machines/rack_middleware/list_filter.rb +39 -0
- data/lib/throttle_machines/rack_middleware/safelist.rb +5 -18
- data/lib/throttle_machines/rack_middleware.rb +2 -4
- data/lib/throttle_machines/storage/base.rb +2 -23
- data/lib/throttle_machines/storage/memory.rb +7 -113
- data/lib/throttle_machines/storage/null.rb +0 -21
- data/lib/throttle_machines/storage/redis/gcra.lua +1 -1
- data/lib/throttle_machines/storage/redis/increment_counter.lua +1 -1
- data/lib/throttle_machines/storage/redis/peek_gcra.lua +1 -1
- data/lib/throttle_machines/storage/redis/peek_token_bucket.lua +1 -1
- data/lib/throttle_machines/storage/redis/token_bucket.lua +1 -1
- data/lib/throttle_machines/storage/redis.rb +5 -68
- data/lib/throttle_machines/version.rb +1 -1
- data/lib/throttle_machines.rb +87 -21
- metadata +74 -15
- data/Rakefile +0 -12
- data/lib/throttle_machines/storage/redis/get_breaker_state.lua +0 -23
- data/lib/throttle_machines/storage/redis/record_breaker_failure.lua +0 -24
- data/lib/throttle_machines/storage/redis/record_breaker_success.lua +0 -16
|
@@ -2,36 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module ThrottleMachines
|
|
4
4
|
class RackMiddleware
|
|
5
|
-
class Fail2Ban
|
|
6
|
-
attr_reader :name, :maxretry, :findtime, :bantime, :block
|
|
7
|
-
|
|
8
|
-
def initialize(name, options, &block)
|
|
9
|
-
@name = name
|
|
10
|
-
@block = block
|
|
11
|
-
|
|
12
|
-
@maxretry = options[:maxretry] || 5
|
|
13
|
-
@findtime = options[:findtime] || 60
|
|
14
|
-
@bantime = options[:bantime] || 300
|
|
15
|
-
end
|
|
16
|
-
|
|
5
|
+
class Fail2Ban < BanFilter
|
|
17
6
|
def banned?(request)
|
|
18
7
|
discriminator = discriminator_for(request)
|
|
19
8
|
return false unless discriminator
|
|
20
9
|
|
|
21
10
|
key = "fail2ban:#{@name}:#{discriminator}"
|
|
22
11
|
|
|
23
|
-
# Use circuit
|
|
24
|
-
breaker =
|
|
12
|
+
# Use a globally managed BreakerMachines circuit as the ban mechanism
|
|
13
|
+
breaker = BreakerMachines::Registry.instance.get_or_create_dynamic_circuit(
|
|
25
14
|
key,
|
|
15
|
+
self,
|
|
26
16
|
failure_threshold: @maxretry,
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
failure_window: @findtime,
|
|
18
|
+
reset_timeout: @bantime
|
|
29
19
|
)
|
|
30
20
|
|
|
31
21
|
# Check if circuit is open (banned)
|
|
32
22
|
if breaker.open?
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
stats = breaker.stats
|
|
24
|
+
now = BreakerMachines.monotonic_time
|
|
25
|
+
time_until_unban = if stats.opened_at
|
|
26
|
+
remaining = @bantime - (now - stats.opened_at)
|
|
27
|
+
remaining.positive? ? remaining : 0
|
|
28
|
+
else
|
|
29
|
+
@bantime
|
|
30
|
+
end
|
|
35
31
|
|
|
36
32
|
request.env['rack.attack.matched'] = @name
|
|
37
33
|
request.env['rack.attack.match_type'] = :fail2ban
|
|
@@ -41,8 +37,8 @@ module ThrottleMachines
|
|
|
41
37
|
maxretry: @maxretry,
|
|
42
38
|
findtime: @findtime,
|
|
43
39
|
bantime: @bantime,
|
|
44
|
-
failures:
|
|
45
|
-
time_until_unban:
|
|
40
|
+
failures: stats.failure_count,
|
|
41
|
+
time_until_unban: time_until_unban
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
ThrottleMachines::RackMiddleware.instrument(request)
|
|
@@ -61,11 +57,12 @@ module ThrottleMachines
|
|
|
61
57
|
# Use the breaker to record a failure if block returns true
|
|
62
58
|
return unless yield
|
|
63
59
|
|
|
64
|
-
breaker =
|
|
60
|
+
breaker = BreakerMachines::Registry.instance.get_or_create_dynamic_circuit(
|
|
65
61
|
key,
|
|
62
|
+
self,
|
|
66
63
|
failure_threshold: @maxretry,
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
failure_window: @findtime,
|
|
65
|
+
reset_timeout: @bantime
|
|
69
66
|
)
|
|
70
67
|
|
|
71
68
|
# Record failure by trying to call through the breaker
|
|
@@ -76,12 +73,6 @@ module ThrottleMachines
|
|
|
76
73
|
# Expected - this records the failure
|
|
77
74
|
end
|
|
78
75
|
end
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
|
|
82
|
-
def discriminator_for(request)
|
|
83
|
-
@block.call(request)
|
|
84
|
-
end
|
|
85
76
|
end
|
|
86
77
|
end
|
|
87
78
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThrottleMachines
|
|
4
|
+
class RackMiddleware
|
|
5
|
+
# Base class for Safelist and Blocklist filters.
|
|
6
|
+
#
|
|
7
|
+
# Both match a request against a user-supplied block and, on a match, tag the
|
|
8
|
+
# rack env and emit instrumentation. They differ only in the +match_type+
|
|
9
|
+
# recorded on the env, which subclasses provide.
|
|
10
|
+
class ListFilter
|
|
11
|
+
attr_reader :name, :block
|
|
12
|
+
|
|
13
|
+
def initialize(name, &block)
|
|
14
|
+
@name = name
|
|
15
|
+
@block = block
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def matched_by?(request)
|
|
19
|
+
return false unless @block
|
|
20
|
+
|
|
21
|
+
if @block.call(request)
|
|
22
|
+
request.env['rack.attack.matched'] = @name
|
|
23
|
+
request.env['rack.attack.match_type'] = match_type
|
|
24
|
+
ThrottleMachines::RackMiddleware.instrument(request)
|
|
25
|
+
true
|
|
26
|
+
else
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @return [Symbol] the rack.attack match type recorded for this filter
|
|
34
|
+
def match_type
|
|
35
|
+
raise NotImplementedError, "#{self.class} must implement #match_type"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -2,25 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module ThrottleMachines
|
|
4
4
|
class RackMiddleware
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# Allows (short-circuits) requests matching the configured block.
|
|
6
|
+
class Safelist < ListFilter
|
|
7
|
+
private
|
|
7
8
|
|
|
8
|
-
def
|
|
9
|
-
|
|
10
|
-
@block = block
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def matched_by?(request)
|
|
14
|
-
return false unless @block
|
|
15
|
-
|
|
16
|
-
if @block.call(request)
|
|
17
|
-
request.env['rack.attack.matched'] = @name
|
|
18
|
-
request.env['rack.attack.match_type'] = :safelist
|
|
19
|
-
ThrottleMachines::RackMiddleware.instrument(request)
|
|
20
|
-
true
|
|
21
|
-
else
|
|
22
|
-
false
|
|
23
|
-
end
|
|
9
|
+
def match_type
|
|
10
|
+
:safelist
|
|
24
11
|
end
|
|
25
12
|
end
|
|
26
13
|
end
|
|
@@ -30,16 +30,14 @@ module ThrottleMachines
|
|
|
30
30
|
:safelists,
|
|
31
31
|
:blocklists
|
|
32
32
|
|
|
33
|
-
def configure(&)
|
|
33
|
+
def configure(&block)
|
|
34
34
|
@configuration ||= Configuration.new
|
|
35
|
-
@configuration.instance_eval(&) if block
|
|
35
|
+
@configuration.instance_eval(&block) if block
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
# rubocop:disable Rails/Delegate -- Ruby 3.4 compatibility issue with delegate
|
|
39
38
|
def reset!
|
|
40
39
|
ThrottleMachines.reset!
|
|
41
40
|
end
|
|
42
|
-
# rubocop:enable Rails/Delegate
|
|
43
41
|
|
|
44
42
|
def clear!
|
|
45
43
|
@configuration = Configuration.new
|
|
@@ -42,27 +42,6 @@ module ThrottleMachines
|
|
|
42
42
|
raise NotImplementedError
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# Circuit breaker operations
|
|
46
|
-
def get_breaker_state(key)
|
|
47
|
-
raise NotImplementedError
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def record_breaker_success(key, timeout, half_open_requests = 1)
|
|
51
|
-
raise NotImplementedError
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def record_breaker_failure(key, threshold, timeout)
|
|
55
|
-
raise NotImplementedError
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def trip_breaker(key, timeout)
|
|
59
|
-
raise NotImplementedError
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def reset_breaker(key)
|
|
63
|
-
raise NotImplementedError
|
|
64
|
-
end
|
|
65
|
-
|
|
66
45
|
# Utility operations
|
|
67
46
|
def clear(pattern = nil)
|
|
68
47
|
raise NotImplementedError
|
|
@@ -72,8 +51,8 @@ module ThrottleMachines
|
|
|
72
51
|
raise NotImplementedError
|
|
73
52
|
end
|
|
74
53
|
|
|
75
|
-
def with_timeout(timeout, &)
|
|
76
|
-
Timeout.timeout(timeout, &)
|
|
54
|
+
def with_timeout(timeout, &block)
|
|
55
|
+
Timeout.timeout(timeout, &block)
|
|
77
56
|
rescue Timeout::Error
|
|
78
57
|
nil
|
|
79
58
|
end
|
|
@@ -10,7 +10,6 @@ module ThrottleMachines
|
|
|
10
10
|
@counters = Concurrent::Hash.new
|
|
11
11
|
@gcra_states = Concurrent::Hash.new
|
|
12
12
|
@token_buckets = Concurrent::Hash.new
|
|
13
|
-
@breaker_states = Concurrent::Hash.new
|
|
14
13
|
|
|
15
14
|
# Use a striped lock pattern - pool of locks for fine-grained concurrency
|
|
16
15
|
@lock_pool_size = options[:lock_pool_size] || 32
|
|
@@ -180,98 +179,7 @@ module ThrottleMachines
|
|
|
180
179
|
end
|
|
181
180
|
end
|
|
182
181
|
|
|
183
|
-
#
|
|
184
|
-
def get_breaker_state(key)
|
|
185
|
-
# First try with read lock
|
|
186
|
-
state = with_read_lock("breaker:#{key}") do
|
|
187
|
-
@breaker_states[key] || { state: :closed, failures: 0, last_failure: nil }
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Check if we need to transition from open to half-open
|
|
191
|
-
if state[:state] == :open && state[:opens_at] && current_time >= state[:opens_at]
|
|
192
|
-
# Release read lock and acquire write lock
|
|
193
|
-
with_write_lock("breaker:#{key}") do
|
|
194
|
-
# Re-check condition after acquiring write lock
|
|
195
|
-
current_state = @breaker_states[key]
|
|
196
|
-
if current_state && current_state[:state] == :open && current_state[:opens_at] && current_time >= current_state[:opens_at]
|
|
197
|
-
@breaker_states[key] = current_state.merge(
|
|
198
|
-
state: :half_open,
|
|
199
|
-
half_open_attempts: 0
|
|
200
|
-
)
|
|
201
|
-
end
|
|
202
|
-
@breaker_states[key] || { state: :closed, failures: 0, last_failure: nil }
|
|
203
|
-
end
|
|
204
|
-
else
|
|
205
|
-
state
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def record_breaker_success(key, _timeout, half_open_requests = 1)
|
|
210
|
-
with_write_lock("breaker:#{key}") do
|
|
211
|
-
state = @breaker_states[key]
|
|
212
|
-
return unless state
|
|
213
|
-
|
|
214
|
-
case state[:state]
|
|
215
|
-
when :half_open
|
|
216
|
-
attempts = (state[:half_open_attempts] || 0) + 1
|
|
217
|
-
if attempts >= half_open_requests
|
|
218
|
-
@breaker_states.delete(key)
|
|
219
|
-
else
|
|
220
|
-
@breaker_states[key] = state.merge(half_open_attempts: attempts)
|
|
221
|
-
end
|
|
222
|
-
when :closed
|
|
223
|
-
# Reset failure count on success
|
|
224
|
-
@breaker_states[key] = state.merge(failures: 0) if state[:failures].positive?
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def record_breaker_failure(key, threshold, timeout)
|
|
230
|
-
with_write_lock("breaker:#{key}") do
|
|
231
|
-
state = @breaker_states[key] || { state: :closed, failures: 0 }
|
|
232
|
-
now = current_time
|
|
233
|
-
|
|
234
|
-
case state[:state]
|
|
235
|
-
when :closed
|
|
236
|
-
failures = state[:failures] + 1
|
|
237
|
-
@breaker_states[key] = if failures >= threshold
|
|
238
|
-
{
|
|
239
|
-
state: :open,
|
|
240
|
-
failures: failures,
|
|
241
|
-
last_failure: now,
|
|
242
|
-
opens_at: now + timeout
|
|
243
|
-
}
|
|
244
|
-
else
|
|
245
|
-
state.merge(failures: failures, last_failure: now)
|
|
246
|
-
end
|
|
247
|
-
when :half_open
|
|
248
|
-
@breaker_states[key] = {
|
|
249
|
-
state: :open,
|
|
250
|
-
failures: state[:failures],
|
|
251
|
-
last_failure: now,
|
|
252
|
-
opens_at: now + timeout
|
|
253
|
-
}
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
@breaker_states[key]
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def trip_breaker(key, timeout)
|
|
261
|
-
with_write_lock("breaker:#{key}") do
|
|
262
|
-
now = current_time
|
|
263
|
-
@breaker_states[key] = {
|
|
264
|
-
state: :open,
|
|
265
|
-
failures: 0,
|
|
266
|
-
last_failure: now,
|
|
267
|
-
opens_at: now + timeout
|
|
268
|
-
}
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def reset_breaker(key)
|
|
273
|
-
with_write_lock("breaker:#{key}") { @breaker_states.delete(key) }
|
|
274
|
-
end
|
|
182
|
+
# No circuit breaker operations here: breaker state is owned by BreakerMachines
|
|
275
183
|
|
|
276
184
|
# Utility operations
|
|
277
185
|
def clear(pattern = nil)
|
|
@@ -279,7 +187,7 @@ module ThrottleMachines
|
|
|
279
187
|
regex = Regexp.new(pattern.gsub('*', '.*'))
|
|
280
188
|
|
|
281
189
|
# Clear matching keys from all stores
|
|
282
|
-
[@counters, @gcra_states, @token_buckets
|
|
190
|
+
[@counters, @gcra_states, @token_buckets].each do |store|
|
|
283
191
|
store.each_key do |k|
|
|
284
192
|
store.delete(k) if k&.match?(regex)
|
|
285
193
|
end
|
|
@@ -288,7 +196,6 @@ module ThrottleMachines
|
|
|
288
196
|
@counters.clear
|
|
289
197
|
@gcra_states.clear
|
|
290
198
|
@token_buckets.clear
|
|
291
|
-
@breaker_states.clear
|
|
292
199
|
end
|
|
293
200
|
end
|
|
294
201
|
|
|
@@ -305,12 +212,12 @@ module ThrottleMachines
|
|
|
305
212
|
|
|
306
213
|
private
|
|
307
214
|
|
|
308
|
-
def with_read_lock(key, &)
|
|
309
|
-
lock_for(key).with_read_lock(&)
|
|
215
|
+
def with_read_lock(key, &block)
|
|
216
|
+
lock_for(key).with_read_lock(&block)
|
|
310
217
|
end
|
|
311
218
|
|
|
312
|
-
def with_write_lock(key, &)
|
|
313
|
-
lock_for(key).with_write_lock(&)
|
|
219
|
+
def with_write_lock(key, &block)
|
|
220
|
+
lock_for(key).with_write_lock(&block)
|
|
314
221
|
end
|
|
315
222
|
|
|
316
223
|
def lock_for(key)
|
|
@@ -350,20 +257,7 @@ module ThrottleMachines
|
|
|
350
257
|
with_write_lock(key) { @token_buckets.delete(key) } if data[:expires_at] && data[:expires_at] <= now
|
|
351
258
|
end
|
|
352
259
|
|
|
353
|
-
#
|
|
354
|
-
@breaker_states.each_pair do |key, data|
|
|
355
|
-
should_delete = false
|
|
356
|
-
|
|
357
|
-
# Clean closed states that have been idle
|
|
358
|
-
should_delete = true if data[:state] == :closed && data[:failures].zero?
|
|
359
|
-
|
|
360
|
-
# Clean expired open states (older than 2x timeout)
|
|
361
|
-
if data[:opens_at] && now > data[:opens_at] + ((data[:opens_at] - (data[:last_failure] || now)) * 2)
|
|
362
|
-
should_delete = true
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
with_write_lock("breaker:#{key}") { @breaker_states.delete(key) } if should_delete
|
|
366
|
-
end
|
|
260
|
+
# No breaker state cleanup: breaker state is managed by BreakerMachines
|
|
367
261
|
rescue StandardError => e
|
|
368
262
|
# Log error but don't crash cleanup thread
|
|
369
263
|
warn "ThrottleMachines: Cleanup error: #{e.message}"
|
|
@@ -54,27 +54,6 @@ module ThrottleMachines
|
|
|
54
54
|
}
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
# Circuit breaker operations
|
|
58
|
-
def get_breaker_state(_key)
|
|
59
|
-
{ state: :closed, failures: 0, last_failure: nil }
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def record_breaker_success(_key, _timeout, _half_open_requests = 1)
|
|
63
|
-
true
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def record_breaker_failure(_key, _threshold, _timeout)
|
|
67
|
-
{ state: :closed, failures: 0, last_failure: nil }
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def trip_breaker(_key, _timeout)
|
|
71
|
-
true
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def reset_breaker(_key)
|
|
75
|
-
true
|
|
76
|
-
end
|
|
77
|
-
|
|
78
57
|
# Utility operations
|
|
79
58
|
def clear(_pattern = nil)
|
|
80
59
|
true
|
|
@@ -11,9 +11,7 @@ module ThrottleMachines
|
|
|
11
11
|
PEEK_GCRA_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'peek_gcra.lua'))
|
|
12
12
|
PEEK_TOKEN_BUCKET_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'peek_token_bucket.lua'))
|
|
13
13
|
INCREMENT_COUNTER_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'increment_counter.lua'))
|
|
14
|
-
|
|
15
|
-
RECORD_BREAKER_SUCCESS_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'record_breaker_success.lua'))
|
|
16
|
-
RECORD_BREAKER_FAILURE_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'record_breaker_failure.lua'))
|
|
14
|
+
# Breaker scripts removed: breaker state is owned by BreakerMachines
|
|
17
15
|
|
|
18
16
|
def initialize(options = {})
|
|
19
17
|
super
|
|
@@ -165,68 +163,7 @@ module ThrottleMachines
|
|
|
165
163
|
retry
|
|
166
164
|
end
|
|
167
165
|
|
|
168
|
-
#
|
|
169
|
-
def get_breaker_state(key)
|
|
170
|
-
breaker_key = prefixed("breaker:#{key}")
|
|
171
|
-
|
|
172
|
-
# Use Lua script for atomic read and potential state transition
|
|
173
|
-
result = with_redis do |redis|
|
|
174
|
-
redis.eval(GET_BREAKER_STATE_SCRIPT, keys: [breaker_key], argv: [current_time])
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
return { state: :closed, failures: 0, last_failure: nil } if result.empty?
|
|
178
|
-
|
|
179
|
-
# Convert hash from Lua to Ruby format
|
|
180
|
-
state = {}
|
|
181
|
-
result.each_slice(2) { |k, v| state[k] = v }
|
|
182
|
-
|
|
183
|
-
{
|
|
184
|
-
state: state['state'].to_sym,
|
|
185
|
-
failures: state['failures'].to_i,
|
|
186
|
-
last_failure: state['last_failure']&.to_f,
|
|
187
|
-
opens_at: state['opens_at']&.to_f,
|
|
188
|
-
half_open_attempts: state['half_open_attempts']&.to_i
|
|
189
|
-
}
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def record_breaker_success(key, _timeout, half_open_requests = 1)
|
|
193
|
-
breaker_key = prefixed("breaker:#{key}")
|
|
194
|
-
|
|
195
|
-
# Use Lua script for atomic success recording
|
|
196
|
-
with_redis do |redis|
|
|
197
|
-
redis.eval(RECORD_BREAKER_SUCCESS_SCRIPT, keys: [breaker_key], argv: [half_open_requests])
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def record_breaker_failure(key, threshold, timeout)
|
|
202
|
-
breaker_key = prefixed("breaker:#{key}")
|
|
203
|
-
now = current_time
|
|
204
|
-
|
|
205
|
-
# Use Lua script for atomic failure recording
|
|
206
|
-
with_redis do |redis|
|
|
207
|
-
redis.eval(RECORD_BREAKER_FAILURE_SCRIPT, keys: [breaker_key], argv: [threshold, timeout, now])
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
get_breaker_state(key)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def trip_breaker(key, timeout)
|
|
214
|
-
breaker_key = prefixed("breaker:#{key}")
|
|
215
|
-
now = current_time
|
|
216
|
-
|
|
217
|
-
with_redis do |redis|
|
|
218
|
-
redis.hmset(breaker_key,
|
|
219
|
-
'state', 'open',
|
|
220
|
-
'failures', 0,
|
|
221
|
-
'last_failure', now,
|
|
222
|
-
'opens_at', now + timeout)
|
|
223
|
-
redis.expire(breaker_key, (timeout * 2).to_i)
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def reset_breaker(key)
|
|
228
|
-
with_redis { |r| r.del(prefixed("breaker:#{key}")) }
|
|
229
|
-
end
|
|
166
|
+
# No circuit breaker operations: breaker state is owned by BreakerMachines
|
|
230
167
|
|
|
231
168
|
# Utility operations
|
|
232
169
|
def clear(pattern = nil)
|
|
@@ -280,13 +217,13 @@ module ThrottleMachines
|
|
|
280
217
|
raise ArgumentError, "Invalid Redis connection: #{e.message}"
|
|
281
218
|
end
|
|
282
219
|
|
|
283
|
-
def with_redis(&)
|
|
220
|
+
def with_redis(&block)
|
|
284
221
|
if @redis.respond_to?(:with)
|
|
285
222
|
# Connection pool
|
|
286
|
-
@redis.with(&)
|
|
223
|
+
@redis.with(&block)
|
|
287
224
|
else
|
|
288
225
|
# Regular Redis client
|
|
289
|
-
|
|
226
|
+
block.call(@redis)
|
|
290
227
|
end
|
|
291
228
|
end
|
|
292
229
|
end
|