throttle_machines 0.1.0 → 0.1.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/lib/throttle_machines/controller_helpers.rb +1 -1
- data/lib/throttle_machines/engine.rb +2 -4
- data/lib/throttle_machines/hedged_breaker.rb +23 -0
- data/lib/throttle_machines/hedged_request.rb +0 -20
- data/lib/throttle_machines/instrumentation.rb +1 -5
- data/lib/throttle_machines/rack_middleware.rb +3 -1
- data/lib/throttle_machines/storage/memory.rb +0 -1
- data/lib/throttle_machines/storage/null.rb +0 -2
- data/lib/throttle_machines/storage/redis/gcra.lua +22 -0
- data/lib/throttle_machines/storage/redis/get_breaker_state.lua +23 -0
- data/lib/throttle_machines/storage/redis/increment_counter.lua +9 -0
- data/lib/throttle_machines/storage/redis/peek_gcra.lua +16 -0
- data/lib/throttle_machines/storage/redis/peek_token_bucket.lua +18 -0
- data/lib/throttle_machines/storage/redis/record_breaker_failure.lua +24 -0
- data/lib/throttle_machines/storage/redis/record_breaker_success.lua +16 -0
- data/lib/throttle_machines/storage/redis/token_bucket.lua +23 -0
- data/lib/throttle_machines/storage/redis.rb +15 -172
- data/lib/throttle_machines/version.rb +1 -1
- data/lib/throttle_machines.rb +2 -6
- metadata +11 -3
- data/lib/throttle_machines/clock.rb +0 -41
- /data/{MIT-LICENSE → LICENSE} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c498f9f097c1eac7384d573ea5e90b04e3850d6b236aae5a0f5b9bb0cdf29ae
|
4
|
+
data.tar.gz: 515fae0aad7083290b12cec368148e021ff9312fb026892fb4574fd89a1a0f6c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec8ac810c8696a93a44bcf305245eef336ce338418b82a5236e73fa919afda7125cee2ac391c0230419f136525ee1104723dc3a81e42517c4c0a48ebd6878d76
|
7
|
+
data.tar.gz: 3908d0866286abc0556b79160c8543da2b542ab3cfb55b2c2d636fc69bb0393196848f920714e89e4926d7e4450ae5de33329d0774ae4a0f8f59860bbbaad620
|
@@ -15,10 +15,8 @@ module ThrottleMachines
|
|
15
15
|
|
16
16
|
initializer 'throttle_machines.configure_defaults' do |_app|
|
17
17
|
ThrottleMachines.configure do |config|
|
18
|
-
# Use
|
19
|
-
|
20
|
-
config.store = ThrottleMachines::Stores::Redis.new(Rails.cache.redis)
|
21
|
-
end
|
18
|
+
# Use Rails.cache , user can override
|
19
|
+
config.store = Rails.cache
|
22
20
|
end
|
23
21
|
end
|
24
22
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ThrottleMachines
|
4
|
+
# Hedged request with circuit breaker integration
|
5
|
+
class HedgedBreaker
|
6
|
+
def initialize(breakers, delay: 0.05)
|
7
|
+
@breakers = Array(breakers)
|
8
|
+
@hedged = HedgedRequest.new(
|
9
|
+
delay: delay,
|
10
|
+
max_attempts: @breakers.size
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(&)
|
15
|
+
@hedged.run do |attempt|
|
16
|
+
breaker = @breakers[attempt]
|
17
|
+
next if breaker.nil?
|
18
|
+
|
19
|
+
breaker.call(&)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -105,26 +105,6 @@ module ThrottleMachines
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
|
108
|
-
# Hedged request with circuit breaker integration
|
109
|
-
class HedgedBreaker
|
110
|
-
def initialize(breakers, delay: 0.05)
|
111
|
-
@breakers = Array(breakers)
|
112
|
-
@hedged = HedgedRequest.new(
|
113
|
-
delay: delay,
|
114
|
-
max_attempts: @breakers.size
|
115
|
-
)
|
116
|
-
end
|
117
|
-
|
118
|
-
def run(&)
|
119
|
-
@hedged.run do |attempt|
|
120
|
-
breaker = @breakers[attempt]
|
121
|
-
next if breaker.nil?
|
122
|
-
|
123
|
-
breaker.call(&)
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
108
|
# Convenience method
|
129
109
|
def self.hedged_request(**, &)
|
130
110
|
hedged = HedgedRequest.new(**)
|
@@ -12,11 +12,7 @@ module ThrottleMachines
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def backend
|
15
|
-
@backend ||=
|
16
|
-
ActiveSupport::Notifications
|
17
|
-
else
|
18
|
-
NullBackend.new
|
19
|
-
end
|
15
|
+
@backend ||= ActiveSupport::Notifications
|
20
16
|
end
|
21
17
|
|
22
18
|
def instrument(event_name, payload = {}, &)
|
@@ -35,9 +35,11 @@ module ThrottleMachines
|
|
35
35
|
@configuration.instance_eval(&) if block
|
36
36
|
end
|
37
37
|
|
38
|
+
# rubocop:disable Rails/Delegate -- Ruby 3.4 compatibility issue with delegate
|
38
39
|
def reset!
|
39
40
|
ThrottleMachines.reset!
|
40
41
|
end
|
42
|
+
# rubocop:enable Rails/Delegate
|
41
43
|
|
42
44
|
def clear!
|
43
45
|
@configuration = Configuration.new
|
@@ -54,7 +56,7 @@ module ThrottleMachines
|
|
54
56
|
|
55
57
|
# Set defaults
|
56
58
|
@enabled = true
|
57
|
-
@notifier = ActiveSupport::Notifications
|
59
|
+
@notifier = ActiveSupport::Notifications
|
58
60
|
@configuration = Configuration.new
|
59
61
|
|
60
62
|
def initialize(app)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
local key = KEYS[1]
|
2
|
+
local emission_interval = tonumber(ARGV[1])
|
3
|
+
local delay_tolerance = tonumber(ARGV[2])
|
4
|
+
local ttl = tonumber(ARGV[3])
|
5
|
+
local now = tonumber(ARGV[4])
|
6
|
+
|
7
|
+
local tat = redis.call('GET', key)
|
8
|
+
if not tat then
|
9
|
+
tat = 0
|
10
|
+
else
|
11
|
+
tat = tonumber(tat)
|
12
|
+
end
|
13
|
+
|
14
|
+
tat = math.max(tat, now)
|
15
|
+
local allow = (tat - now) <= delay_tolerance
|
16
|
+
|
17
|
+
if allow then
|
18
|
+
local new_tat = tat + emission_interval
|
19
|
+
redis.call('SET', key, new_tat, 'EX', ttl)
|
20
|
+
end
|
21
|
+
|
22
|
+
return { allow and 1 or 0, tat }
|
@@ -0,0 +1,23 @@
|
|
1
|
+
local data = redis.call('HGETALL', KEYS[1])
|
2
|
+
if #data == 0 then
|
3
|
+
return {}
|
4
|
+
end
|
5
|
+
|
6
|
+
local state = {}
|
7
|
+
for i = 1, #data, 2 do
|
8
|
+
state[data[i]] = data[i + 1]
|
9
|
+
end
|
10
|
+
|
11
|
+
-- Auto-transition from open to half-open if timeout passed
|
12
|
+
if state['state'] == 'open' and state['opens_at'] then
|
13
|
+
local now = tonumber(ARGV[1])
|
14
|
+
local opens_at = tonumber(state['opens_at'])
|
15
|
+
|
16
|
+
if now >= opens_at then
|
17
|
+
redis.call('HSET', KEYS[1], 'state', 'half_open', 'half_open_attempts', '0')
|
18
|
+
state['state'] = 'half_open'
|
19
|
+
state['half_open_attempts'] = '0'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
return state
|
@@ -0,0 +1,16 @@
|
|
1
|
+
local key = KEYS[1]
|
2
|
+
local emission_interval = tonumber(ARGV[1])
|
3
|
+
local delay_tolerance = tonumber(ARGV[2])
|
4
|
+
local now = tonumber(ARGV[3])
|
5
|
+
|
6
|
+
local tat = redis.call('GET', key)
|
7
|
+
if not tat then
|
8
|
+
tat = 0
|
9
|
+
else
|
10
|
+
tat = tonumber(tat)
|
11
|
+
end
|
12
|
+
|
13
|
+
tat = math.max(tat, now)
|
14
|
+
local allow = (tat - now) <= delay_tolerance
|
15
|
+
|
16
|
+
return { allow and 1 or 0, tat }
|
@@ -0,0 +1,18 @@
|
|
1
|
+
local key = KEYS[1]
|
2
|
+
local capacity = tonumber(ARGV[1])
|
3
|
+
local refill_rate = tonumber(ARGV[2])
|
4
|
+
local now = tonumber(ARGV[3])
|
5
|
+
|
6
|
+
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
|
7
|
+
local tokens = tonumber(bucket[1]) or capacity
|
8
|
+
local last_refill = tonumber(bucket[2]) or now
|
9
|
+
|
10
|
+
-- Calculate tokens without modifying
|
11
|
+
local elapsed = now - last_refill
|
12
|
+
local tokens_to_add = elapsed * refill_rate
|
13
|
+
tokens = math.min(tokens + tokens_to_add, capacity)
|
14
|
+
|
15
|
+
local allow = tokens >= 1
|
16
|
+
local tokens_after = allow and (tokens - 1) or 0
|
17
|
+
|
18
|
+
return { allow and 1 or 0, tokens_after }
|
@@ -0,0 +1,24 @@
|
|
1
|
+
local state = redis.call('HGET', KEYS[1], 'state') or 'closed'
|
2
|
+
local now = ARGV[3]
|
3
|
+
local timeout = tonumber(ARGV[2])
|
4
|
+
|
5
|
+
if state == 'half_open' then
|
6
|
+
-- Failure in half-open state, just re-open the circuit
|
7
|
+
redis.call('HMSET', KEYS[1],
|
8
|
+
'state', 'open',
|
9
|
+
'opens_at', tonumber(now) + timeout,
|
10
|
+
'last_failure', now
|
11
|
+
)
|
12
|
+
else -- state is 'closed' or nil
|
13
|
+
local failures = redis.call('HINCRBY', KEYS[1], 'failures', 1)
|
14
|
+
redis.call('HSET', KEYS[1], 'last_failure', now)
|
15
|
+
|
16
|
+
if failures >= tonumber(ARGV[1]) then
|
17
|
+
redis.call('HMSET', KEYS[1],
|
18
|
+
'state', 'open',
|
19
|
+
'opens_at', tonumber(now) + timeout
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
redis.call('EXPIRE', KEYS[1], timeout * 2)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
local state = redis.call('HGET', KEYS[1], 'state')
|
2
|
+
|
3
|
+
if state == 'half_open' then
|
4
|
+
-- Increment half-open attempts and potentially close the circuit
|
5
|
+
local attempts = redis.call('HINCRBY', KEYS[1], 'half_open_attempts', 1)
|
6
|
+
|
7
|
+
if attempts >= tonumber(ARGV[1]) then
|
8
|
+
redis.call('DEL', KEYS[1])
|
9
|
+
end
|
10
|
+
elseif state == 'closed' then
|
11
|
+
-- Reset failure count on success in closed state
|
12
|
+
local failures = redis.call('HGET', KEYS[1], 'failures')
|
13
|
+
if failures and tonumber(failures) > 0 then
|
14
|
+
redis.call('HSET', KEYS[1], 'failures', 0)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
local key = KEYS[1]
|
2
|
+
local capacity = tonumber(ARGV[1])
|
3
|
+
local refill_rate = tonumber(ARGV[2])
|
4
|
+
local ttl = tonumber(ARGV[3])
|
5
|
+
local now = tonumber(ARGV[4])
|
6
|
+
|
7
|
+
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
|
8
|
+
local tokens = tonumber(bucket[1]) or capacity
|
9
|
+
local last_refill = tonumber(bucket[2]) or now
|
10
|
+
|
11
|
+
-- Refill tokens
|
12
|
+
local elapsed = now - last_refill
|
13
|
+
local tokens_to_add = elapsed * refill_rate
|
14
|
+
tokens = math.min(tokens + tokens_to_add, capacity)
|
15
|
+
|
16
|
+
local allow = tokens >= 1
|
17
|
+
if allow then
|
18
|
+
tokens = tokens - 1
|
19
|
+
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
|
20
|
+
redis.call('EXPIRE', key, ttl)
|
21
|
+
end
|
22
|
+
|
23
|
+
return { allow and 1 or 0, tokens }
|
@@ -1,100 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'base'
|
4
|
-
|
5
3
|
module ThrottleMachines
|
6
4
|
module Storage
|
7
5
|
class Redis < Base
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
tat = tonumber(tat)
|
20
|
-
end
|
21
|
-
|
22
|
-
tat = math.max(tat, now)
|
23
|
-
local allow = (tat - now) <= delay_tolerance
|
24
|
-
|
25
|
-
if allow then
|
26
|
-
local new_tat = tat + emission_interval
|
27
|
-
redis.call('SET', key, new_tat, 'EX', ttl)
|
28
|
-
end
|
29
|
-
|
30
|
-
return { allow and 1 or 0, tat }
|
31
|
-
LUA
|
32
|
-
|
33
|
-
TOKEN_BUCKET_SCRIPT = <<~LUA
|
34
|
-
local key = KEYS[1]
|
35
|
-
local capacity = tonumber(ARGV[1])
|
36
|
-
local refill_rate = tonumber(ARGV[2])
|
37
|
-
local ttl = tonumber(ARGV[3])
|
38
|
-
local now = tonumber(ARGV[4])
|
39
|
-
|
40
|
-
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
|
41
|
-
local tokens = tonumber(bucket[1]) or capacity
|
42
|
-
local last_refill = tonumber(bucket[2]) or now
|
43
|
-
|
44
|
-
-- Refill tokens
|
45
|
-
local elapsed = now - last_refill
|
46
|
-
local tokens_to_add = elapsed * refill_rate
|
47
|
-
tokens = math.min(tokens + tokens_to_add, capacity)
|
48
|
-
|
49
|
-
local allow = tokens >= 1
|
50
|
-
if allow then
|
51
|
-
tokens = tokens - 1
|
52
|
-
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
|
53
|
-
redis.call('EXPIRE', key, ttl)
|
54
|
-
end
|
55
|
-
|
56
|
-
return { allow and 1 or 0, tokens }
|
57
|
-
LUA
|
58
|
-
|
59
|
-
PEEK_GCRA_SCRIPT = <<~LUA
|
60
|
-
local key = KEYS[1]
|
61
|
-
local emission_interval = tonumber(ARGV[1])
|
62
|
-
local delay_tolerance = tonumber(ARGV[2])
|
63
|
-
local now = tonumber(ARGV[3])
|
64
|
-
|
65
|
-
local tat = redis.call('GET', key)
|
66
|
-
if not tat then
|
67
|
-
tat = 0
|
68
|
-
else
|
69
|
-
tat = tonumber(tat)
|
70
|
-
end
|
71
|
-
|
72
|
-
tat = math.max(tat, now)
|
73
|
-
local allow = (tat - now) <= delay_tolerance
|
74
|
-
|
75
|
-
return { allow and 1 or 0, tat }
|
76
|
-
LUA
|
77
|
-
|
78
|
-
PEEK_TOKEN_BUCKET_SCRIPT = <<~LUA
|
79
|
-
local key = KEYS[1]
|
80
|
-
local capacity = tonumber(ARGV[1])
|
81
|
-
local refill_rate = tonumber(ARGV[2])
|
82
|
-
local now = tonumber(ARGV[3])
|
83
|
-
|
84
|
-
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
|
85
|
-
local tokens = tonumber(bucket[1]) or capacity
|
86
|
-
local last_refill = tonumber(bucket[2]) or now
|
87
|
-
|
88
|
-
-- Calculate tokens without modifying
|
89
|
-
local elapsed = now - last_refill
|
90
|
-
local tokens_to_add = elapsed * refill_rate
|
91
|
-
tokens = math.min(tokens + tokens_to_add, capacity)
|
92
|
-
|
93
|
-
local allow = tokens >= 1
|
94
|
-
local tokens_after = allow and (tokens - 1) or 0
|
95
|
-
|
96
|
-
return { allow and 1 or 0, tokens_after }
|
97
|
-
LUA
|
6
|
+
# Load Lua scripts from files
|
7
|
+
LUA_SCRIPTS_DIR = File.expand_path('redis', __dir__)
|
8
|
+
|
9
|
+
GCRA_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'gcra.lua'))
|
10
|
+
TOKEN_BUCKET_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'token_bucket.lua'))
|
11
|
+
PEEK_GCRA_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'peek_gcra.lua'))
|
12
|
+
PEEK_TOKEN_BUCKET_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'peek_token_bucket.lua'))
|
13
|
+
INCREMENT_COUNTER_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'increment_counter.lua'))
|
14
|
+
GET_BREAKER_STATE_SCRIPT = File.read(File.join(LUA_SCRIPTS_DIR, 'get_breaker_state.lua'))
|
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'))
|
98
17
|
|
99
18
|
def initialize(options = {})
|
100
19
|
super
|
@@ -117,17 +36,7 @@ module ThrottleMachines
|
|
117
36
|
|
118
37
|
# Use Lua script for atomic increment with TTL
|
119
38
|
with_redis do |redis|
|
120
|
-
redis.eval(
|
121
|
-
local count = redis.call('INCRBY', KEYS[1], ARGV[1])
|
122
|
-
local ttl = redis.call('TTL', KEYS[1])
|
123
|
-
|
124
|
-
-- Set expiry if key is new (ttl == -2) or has no TTL (ttl == -1)
|
125
|
-
if ttl <= 0 then
|
126
|
-
redis.call('EXPIRE', KEYS[1], ARGV[2])
|
127
|
-
end
|
128
|
-
|
129
|
-
return count
|
130
|
-
LUA
|
39
|
+
redis.eval(INCREMENT_COUNTER_SCRIPT, keys: [window_key], argv: [amount, window.to_i])
|
131
40
|
end
|
132
41
|
end
|
133
42
|
|
@@ -262,31 +171,7 @@ module ThrottleMachines
|
|
262
171
|
|
263
172
|
# Use Lua script for atomic read and potential state transition
|
264
173
|
result = with_redis do |redis|
|
265
|
-
redis.eval(
|
266
|
-
local data = redis.call('HGETALL', KEYS[1])
|
267
|
-
if #data == 0 then
|
268
|
-
return {}
|
269
|
-
end
|
270
|
-
|
271
|
-
local state = {}
|
272
|
-
for i = 1, #data, 2 do
|
273
|
-
state[data[i]] = data[i + 1]
|
274
|
-
end
|
275
|
-
|
276
|
-
-- Auto-transition from open to half-open if timeout passed
|
277
|
-
if state['state'] == 'open' and state['opens_at'] then
|
278
|
-
local now = tonumber(ARGV[1])
|
279
|
-
local opens_at = tonumber(state['opens_at'])
|
280
|
-
#{' '}
|
281
|
-
if now >= opens_at then
|
282
|
-
redis.call('HSET', KEYS[1], 'state', 'half_open', 'half_open_attempts', '0')
|
283
|
-
state['state'] = 'half_open'
|
284
|
-
state['half_open_attempts'] = '0'
|
285
|
-
end
|
286
|
-
end
|
287
|
-
|
288
|
-
return state
|
289
|
-
LUA
|
174
|
+
redis.eval(GET_BREAKER_STATE_SCRIPT, keys: [breaker_key], argv: [current_time])
|
290
175
|
end
|
291
176
|
|
292
177
|
return { state: :closed, failures: 0, last_failure: nil } if result.empty?
|
@@ -309,24 +194,7 @@ module ThrottleMachines
|
|
309
194
|
|
310
195
|
# Use Lua script for atomic success recording
|
311
196
|
with_redis do |redis|
|
312
|
-
redis.eval(
|
313
|
-
local state = redis.call('HGET', KEYS[1], 'state')
|
314
|
-
|
315
|
-
if state == 'half_open' then
|
316
|
-
-- Increment half-open attempts and potentially close the circuit
|
317
|
-
local attempts = redis.call('HINCRBY', KEYS[1], 'half_open_attempts', 1)
|
318
|
-
#{' '}
|
319
|
-
if attempts >= tonumber(ARGV[1]) then
|
320
|
-
redis.call('DEL', KEYS[1])
|
321
|
-
end
|
322
|
-
elseif state == 'closed' then
|
323
|
-
-- Reset failure count on success in closed state
|
324
|
-
local failures = redis.call('HGET', KEYS[1], 'failures')
|
325
|
-
if failures and tonumber(failures) > 0 then
|
326
|
-
redis.call('HSET', KEYS[1], 'failures', 0)
|
327
|
-
end
|
328
|
-
end
|
329
|
-
LUA
|
197
|
+
redis.eval(RECORD_BREAKER_SUCCESS_SCRIPT, keys: [breaker_key], argv: [half_open_requests])
|
330
198
|
end
|
331
199
|
end
|
332
200
|
|
@@ -336,32 +204,7 @@ module ThrottleMachines
|
|
336
204
|
|
337
205
|
# Use Lua script for atomic failure recording
|
338
206
|
with_redis do |redis|
|
339
|
-
redis.eval(
|
340
|
-
local state = redis.call('HGET', KEYS[1], 'state') or 'closed'
|
341
|
-
local now = ARGV[3]
|
342
|
-
local timeout = tonumber(ARGV[2])
|
343
|
-
|
344
|
-
if state == 'half_open' then
|
345
|
-
-- Failure in half-open state, just re-open the circuit
|
346
|
-
redis.call('HMSET', KEYS[1],
|
347
|
-
'state', 'open',
|
348
|
-
'opens_at', tonumber(now) + timeout,
|
349
|
-
'last_failure', now
|
350
|
-
)
|
351
|
-
else -- state is 'closed' or nil
|
352
|
-
local failures = redis.call('HINCRBY', KEYS[1], 'failures', 1)
|
353
|
-
redis.call('HSET', KEYS[1], 'last_failure', now)
|
354
|
-
#{' '}
|
355
|
-
if failures >= tonumber(ARGV[1]) then
|
356
|
-
redis.call('HMSET', KEYS[1],
|
357
|
-
'state', 'open',
|
358
|
-
'opens_at', tonumber(now) + timeout
|
359
|
-
)
|
360
|
-
end
|
361
|
-
end
|
362
|
-
|
363
|
-
redis.call('EXPIRE', KEYS[1], timeout * 2)
|
364
|
-
LUA
|
207
|
+
redis.eval(RECORD_BREAKER_FAILURE_SCRIPT, keys: [breaker_key], argv: [threshold, timeout, now])
|
365
208
|
end
|
366
209
|
|
367
210
|
get_breaker_state(key)
|
data/lib/throttle_machines.rb
CHANGED
@@ -14,9 +14,6 @@ loader = Zeitwerk::Loader.for_gem
|
|
14
14
|
loader.ignore("#{__dir__}/throttle_machines/engine.rb") unless defined?(Rails::Engine)
|
15
15
|
loader.setup
|
16
16
|
|
17
|
-
# Load conditional dependencies manually
|
18
|
-
require_relative 'throttle_machines/storage/redis' if defined?(Redis)
|
19
|
-
|
20
17
|
module ThrottleMachines
|
21
18
|
include ActiveSupport::Configurable
|
22
19
|
|
@@ -132,7 +129,6 @@ module ThrottleMachines
|
|
132
129
|
# Auto-configure with defaults
|
133
130
|
configure
|
134
131
|
|
135
|
-
|
136
|
-
|
137
|
-
RetryExhaustedError = ChronoMachines::MaxRetriesExceededError if defined?(ChronoMachines::MaxRetriesExceededError)
|
132
|
+
CircuitOpenError = BreakerMachines::CircuitOpenError
|
133
|
+
RetryExhaustedError = ChronoMachines::MaxRetriesExceededError
|
138
134
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: throttle_machines
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -101,16 +101,16 @@ executables: []
|
|
101
101
|
extensions: []
|
102
102
|
extra_rdoc_files: []
|
103
103
|
files:
|
104
|
-
-
|
104
|
+
- LICENSE
|
105
105
|
- README.md
|
106
106
|
- Rakefile
|
107
107
|
- lib/throttle_machines.rb
|
108
108
|
- lib/throttle_machines/async_limiter.rb
|
109
|
-
- lib/throttle_machines/clock.rb
|
110
109
|
- lib/throttle_machines/control.rb
|
111
110
|
- lib/throttle_machines/controller_helpers.rb
|
112
111
|
- lib/throttle_machines/dependency_error.rb
|
113
112
|
- lib/throttle_machines/engine.rb
|
113
|
+
- lib/throttle_machines/hedged_breaker.rb
|
114
114
|
- lib/throttle_machines/hedged_request.rb
|
115
115
|
- lib/throttle_machines/instrumentation.rb
|
116
116
|
- lib/throttle_machines/limiter.rb
|
@@ -128,6 +128,14 @@ files:
|
|
128
128
|
- lib/throttle_machines/storage/memory.rb
|
129
129
|
- lib/throttle_machines/storage/null.rb
|
130
130
|
- lib/throttle_machines/storage/redis.rb
|
131
|
+
- lib/throttle_machines/storage/redis/gcra.lua
|
132
|
+
- lib/throttle_machines/storage/redis/get_breaker_state.lua
|
133
|
+
- lib/throttle_machines/storage/redis/increment_counter.lua
|
134
|
+
- lib/throttle_machines/storage/redis/peek_gcra.lua
|
135
|
+
- lib/throttle_machines/storage/redis/peek_token_bucket.lua
|
136
|
+
- lib/throttle_machines/storage/redis/record_breaker_failure.lua
|
137
|
+
- lib/throttle_machines/storage/redis/record_breaker_success.lua
|
138
|
+
- lib/throttle_machines/storage/redis/token_bucket.lua
|
131
139
|
- lib/throttle_machines/throttled_error.rb
|
132
140
|
- lib/throttle_machines/version.rb
|
133
141
|
homepage: https://github.com/seuros/throttle_machines
|
@@ -1,41 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ThrottleMachines
|
4
|
-
# TestClock for time manipulation in tests
|
5
|
-
# This overrides the global monotonic_time method for testing
|
6
|
-
class TestClock
|
7
|
-
attr_accessor :current_time
|
8
|
-
|
9
|
-
def initialize(start_time = Time.now.to_f)
|
10
|
-
@current_time = start_time
|
11
|
-
|
12
|
-
# Override the global monotonic_time method
|
13
|
-
ThrottleMachines.singleton_class.define_method(:monotonic_time) do
|
14
|
-
@current_time
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def now
|
19
|
-
@current_time
|
20
|
-
end
|
21
|
-
|
22
|
-
def monotonic
|
23
|
-
@current_time
|
24
|
-
end
|
25
|
-
|
26
|
-
def advance(seconds)
|
27
|
-
@current_time += seconds
|
28
|
-
end
|
29
|
-
|
30
|
-
def travel_to(time)
|
31
|
-
@current_time = time.to_f
|
32
|
-
end
|
33
|
-
|
34
|
-
def reset
|
35
|
-
# Restore the original monotonic_time method
|
36
|
-
ThrottleMachines.singleton_class.define_method(:monotonic_time) do
|
37
|
-
BreakerMachines.monotonic_time
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
/data/{MIT-LICENSE → LICENSE}
RENAMED
File without changes
|