throttle_machines 0.3.0-x86_64-linux → 0.3.1-x86_64-linux
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/storage/base.rb +20 -0
- data/lib/throttle_machines/storage/memory.rb +6 -6
- data/lib/throttle_machines/storage/redis/gcra.lua +6 -2
- data/lib/throttle_machines/storage/redis/peek_gcra.lua +6 -2
- data/lib/throttle_machines/storage/redis/peek_token_bucket.lua +9 -3
- data/lib/throttle_machines/storage/redis/token_bucket.lua +7 -3
- data/lib/throttle_machines/storage/redis.rb +21 -22
- data/lib/throttle_machines/version.rb +1 -1
- data/lib/throttle_machines_native/throttle_machines_native.so +0 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0e7b82b0a0e6aee13b28e5dff8d398583123ae16859094db1f5731563b6756a
|
|
4
|
+
data.tar.gz: 55eae369f9ff8d44e771ce1d30af98e81468c4e4590cb283121236dad134d726
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e2199d9db95a4b57fc42ed9d702a464e7efccc6a43312ef0e46e33b16cba5ec19bf6a1b684a8eaab2463d0ae89a4fcae063571a198a48b2d53a526f7a5730c5
|
|
7
|
+
data.tar.gz: b10d373aa0a749dddea669a8f50d1118c3101fb68b84a1411eed792457edae55e0f8cb0ecf43a389da389bc5884a9cead734df1a4a89687eb1027765ddc2bd56
|
|
@@ -57,8 +57,28 @@ module ThrottleMachines
|
|
|
57
57
|
nil
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
# Rounding slack for time comparisons, in units of the clock's precision.
|
|
61
|
+
# Matches the native core.
|
|
62
|
+
SLACK_ULPS = 64
|
|
63
|
+
|
|
60
64
|
protected
|
|
61
65
|
|
|
66
|
+
# Times are running sums and differences of float seconds, so an
|
|
67
|
+
# on-schedule arrival can miss an exact boundary by a few ulps. The slack
|
|
68
|
+
# scales with the clock's magnitude.
|
|
69
|
+
def rounding_slack(time)
|
|
70
|
+
Float::EPSILON * SLACK_ULPS * [time.abs, 1.0].max
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def gcra_allowed?(tat, now, delay_tolerance)
|
|
74
|
+
tat - now <= delay_tolerance + rounding_slack(tat)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Whether a token is available, allowing for rounding in the refill.
|
|
78
|
+
def token_available?(tokens, now, refill_rate)
|
|
79
|
+
tokens >= 1 - (rounding_slack(now) * refill_rate)
|
|
80
|
+
end
|
|
81
|
+
|
|
62
82
|
def current_time
|
|
63
83
|
# Use monotonic time for consistency with BreakerMachines
|
|
64
84
|
ThrottleMachines.monotonic_time
|
|
@@ -86,7 +86,7 @@ module ThrottleMachines
|
|
|
86
86
|
state = @gcra_states[key] || { tat: 0.0 }
|
|
87
87
|
|
|
88
88
|
tat = [state[:tat], now].max
|
|
89
|
-
allow = tat
|
|
89
|
+
allow = gcra_allowed?(tat, now, delay_tolerance)
|
|
90
90
|
|
|
91
91
|
if allow
|
|
92
92
|
new_tat = tat + emission_interval
|
|
@@ -107,7 +107,7 @@ module ThrottleMachines
|
|
|
107
107
|
state = @gcra_states[key] || { tat: 0.0 }
|
|
108
108
|
|
|
109
109
|
tat = [state[:tat], now].max
|
|
110
|
-
allow = tat
|
|
110
|
+
allow = gcra_allowed?(tat, now, delay_tolerance)
|
|
111
111
|
|
|
112
112
|
{
|
|
113
113
|
allowed: allow,
|
|
@@ -130,8 +130,8 @@ module ThrottleMachines
|
|
|
130
130
|
bucket[:last_refill] = now
|
|
131
131
|
|
|
132
132
|
# Check if we can consume a token
|
|
133
|
-
if bucket[:tokens]
|
|
134
|
-
bucket[:tokens]
|
|
133
|
+
if token_available?(bucket[:tokens], now, refill_rate)
|
|
134
|
+
bucket[:tokens] = [bucket[:tokens] - 1, 0.0].max
|
|
135
135
|
@token_buckets[key] = bucket.merge(expires_at: now + ttl)
|
|
136
136
|
|
|
137
137
|
{
|
|
@@ -161,11 +161,11 @@ module ThrottleMachines
|
|
|
161
161
|
tokens_to_add = elapsed * refill_rate
|
|
162
162
|
current_tokens = [bucket[:tokens] + tokens_to_add, capacity].min
|
|
163
163
|
|
|
164
|
-
if current_tokens
|
|
164
|
+
if token_available?(current_tokens, now, refill_rate)
|
|
165
165
|
{
|
|
166
166
|
allowed: true,
|
|
167
167
|
retry_after: 0,
|
|
168
|
-
tokens_remaining:
|
|
168
|
+
tokens_remaining: [current_tokens - 1, 0].max.floor
|
|
169
169
|
}
|
|
170
170
|
else
|
|
171
171
|
retry_after = (1 - current_tokens) / refill_rate
|
|
@@ -12,11 +12,15 @@ else
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
tat = math.max(tat, now)
|
|
15
|
-
|
|
15
|
+
-- The TAT is a running sum of emission intervals, so an on-schedule arrival
|
|
16
|
+
-- can overshoot the tolerance by a few ulps; the slack scales with the clock.
|
|
17
|
+
local slack = 2.220446049250313e-16 * 64 * math.max(math.abs(tat), 1)
|
|
18
|
+
local allow = (tat - now) <= delay_tolerance + slack
|
|
16
19
|
|
|
17
20
|
if allow then
|
|
18
21
|
local new_tat = tat + emission_interval
|
|
19
22
|
redis.call('SET', key, new_tat, 'EX', ttl)
|
|
20
23
|
end
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
-- Redis truncates Lua numbers in replies to integers; send the TAT as text.
|
|
26
|
+
return { allow and 1 or 0, string.format('%.17g', tat) }
|
|
@@ -11,6 +11,10 @@ else
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
tat = math.max(tat, now)
|
|
14
|
-
|
|
14
|
+
-- The TAT is a running sum of emission intervals, so an on-schedule arrival
|
|
15
|
+
-- can overshoot the tolerance by a few ulps; the slack scales with the clock.
|
|
16
|
+
local slack = 2.220446049250313e-16 * 64 * math.max(math.abs(tat), 1)
|
|
17
|
+
local allow = (tat - now) <= delay_tolerance + slack
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
-- Redis truncates Lua numbers in replies to integers; send the TAT as text.
|
|
20
|
+
return { allow and 1 or 0, string.format('%.17g', tat) }
|
|
@@ -12,7 +12,13 @@ local elapsed = now - last_refill
|
|
|
12
12
|
local tokens_to_add = elapsed * refill_rate
|
|
13
13
|
tokens = math.min(tokens + tokens_to_add, capacity)
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
-- `elapsed` carries the clock's rounding; a token due exactly now may come
|
|
16
|
+
-- out a hair short of 1. The slack scales with the clock, as in the native core.
|
|
17
|
+
local slack = 2.220446049250313e-16 * 64 * math.max(math.abs(now), 1) * refill_rate
|
|
18
|
+
local allow = tokens >= 1 - slack
|
|
19
|
+
-- Admitted: tokens left after this request. Refused: the fractional balance,
|
|
20
|
+
-- which retry_after is computed from.
|
|
21
|
+
local tokens_after = allow and math.max(tokens - 1, 0) or tokens
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
-- Redis truncates Lua numbers in replies to integers; send the count as text.
|
|
24
|
+
return { allow and 1 or 0, string.format('%.17g', tokens_after) }
|
|
@@ -13,11 +13,15 @@ local elapsed = now - last_refill
|
|
|
13
13
|
local tokens_to_add = elapsed * refill_rate
|
|
14
14
|
tokens = math.min(tokens + tokens_to_add, capacity)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
-- `elapsed` carries the clock's rounding; a token due exactly now may come
|
|
17
|
+
-- out a hair short of 1. The slack scales with the clock, as in the native core.
|
|
18
|
+
local slack = 2.220446049250313e-16 * 64 * math.max(math.abs(now), 1) * refill_rate
|
|
19
|
+
local allow = tokens >= 1 - slack
|
|
17
20
|
if allow then
|
|
18
|
-
tokens = tokens - 1
|
|
21
|
+
tokens = math.max(tokens - 1, 0)
|
|
19
22
|
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
|
|
20
23
|
redis.call('EXPIRE', key, ttl)
|
|
21
24
|
end
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
-- Redis truncates Lua numbers in replies to integers; send the count as text.
|
|
27
|
+
return { allow and 1 or 0, string.format('%.17g', tokens) }
|
|
@@ -59,13 +59,13 @@ module ThrottleMachines
|
|
|
59
59
|
|
|
60
60
|
# GCRA operations (atomic via Lua)
|
|
61
61
|
def check_gcra_limit(key, emission_interval, delay_tolerance, ttl)
|
|
62
|
+
now = current_time
|
|
62
63
|
result = run_script(GCRA_SCRIPT, GCRA_SHA,
|
|
63
64
|
keys: [prefixed(key)],
|
|
64
|
-
argv: [emission_interval, delay_tolerance, ttl,
|
|
65
|
+
argv: [emission_interval, delay_tolerance, ttl, now])
|
|
65
66
|
|
|
66
67
|
allowed = result[0] == 1
|
|
67
|
-
tat = result[1]
|
|
68
|
-
now = current_time
|
|
68
|
+
tat = Float(result[1])
|
|
69
69
|
|
|
70
70
|
{
|
|
71
71
|
allowed: allowed,
|
|
@@ -80,25 +80,18 @@ module ThrottleMachines
|
|
|
80
80
|
keys: [prefixed(key)],
|
|
81
81
|
argv: [capacity, refill_rate, ttl, current_time])
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
tokens = result[1]
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
allowed: allowed,
|
|
88
|
-
retry_after: allowed ? 0 : (1 - tokens) / refill_rate,
|
|
89
|
-
tokens_remaining: tokens.floor
|
|
90
|
-
}
|
|
83
|
+
token_bucket_result(result, refill_rate)
|
|
91
84
|
end
|
|
92
85
|
|
|
93
86
|
# Peek methods for non-consuming checks
|
|
94
87
|
def peek_gcra_limit(key, emission_interval, delay_tolerance)
|
|
88
|
+
now = current_time
|
|
95
89
|
result = run_script(PEEK_GCRA_SCRIPT, PEEK_GCRA_SHA,
|
|
96
90
|
keys: [prefixed(key)],
|
|
97
|
-
argv: [emission_interval, delay_tolerance,
|
|
91
|
+
argv: [emission_interval, delay_tolerance, now])
|
|
98
92
|
|
|
99
93
|
allowed = result[0] == 1
|
|
100
|
-
tat = result[1]
|
|
101
|
-
now = current_time
|
|
94
|
+
tat = Float(result[1])
|
|
102
95
|
|
|
103
96
|
{
|
|
104
97
|
allowed: allowed,
|
|
@@ -112,14 +105,7 @@ module ThrottleMachines
|
|
|
112
105
|
keys: [prefixed(key)],
|
|
113
106
|
argv: [capacity, refill_rate, current_time])
|
|
114
107
|
|
|
115
|
-
|
|
116
|
-
tokens_remaining = result[1]
|
|
117
|
-
|
|
118
|
-
{
|
|
119
|
-
allowed: allowed,
|
|
120
|
-
retry_after: allowed ? 0 : (1 - tokens_remaining) / refill_rate,
|
|
121
|
-
tokens_remaining: tokens_remaining.floor
|
|
122
|
-
}
|
|
108
|
+
token_bucket_result(result, refill_rate)
|
|
123
109
|
end
|
|
124
110
|
|
|
125
111
|
# No circuit breaker operations: breaker state is owned by BreakerMachines
|
|
@@ -147,6 +133,19 @@ module ThrottleMachines
|
|
|
147
133
|
|
|
148
134
|
private
|
|
149
135
|
|
|
136
|
+
# `result` is [allowed, tokens]: the balance after an admission, or the
|
|
137
|
+
# fractional balance a refusal waits on. Matches Storage::Memory.
|
|
138
|
+
def token_bucket_result(result, refill_rate)
|
|
139
|
+
allowed = result[0] == 1
|
|
140
|
+
tokens = Float(result[1])
|
|
141
|
+
|
|
142
|
+
{
|
|
143
|
+
allowed: allowed,
|
|
144
|
+
retry_after: allowed ? 0 : (1 - tokens) / refill_rate,
|
|
145
|
+
tokens_remaining: allowed ? tokens.floor : 0
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
150
149
|
def prefixed(key)
|
|
151
150
|
"#{@prefix}#{key}"
|
|
152
151
|
end
|
|
Binary file
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: throttle_machines
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|