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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6930fcc4385b64ab5aaa9fd9ebbaa1b001fd8a55872e84eb70b94fbeb028f9bd
4
- data.tar.gz: a3bdbd07e82ada0f96f3e402c0c4a9e59daeabc592f750dcca1e4e6af8d4bbc8
3
+ metadata.gz: c0e7b82b0a0e6aee13b28e5dff8d398583123ae16859094db1f5731563b6756a
4
+ data.tar.gz: 55eae369f9ff8d44e771ce1d30af98e81468c4e4590cb283121236dad134d726
5
5
  SHA512:
6
- metadata.gz: 1a408d9c1aedf8b8ae9ec3aaa212c707dc1526907f401b8c1118e0b040eb4fc161cc17b205be3f098864c02364535103ccead629f58a9d3deef1fe32f240f1d3
7
- data.tar.gz: 859411fc180acee7b70865e9f95f20b4de8fd14ba3f1e1c48b35d612ce2455152265c76335aa722b84ee0065aaf62ca95af029b7213a14a2bf591ae5b6f76797
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 - now <= delay_tolerance
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 - now <= delay_tolerance
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] >= 1
134
- bucket[:tokens] -= 1
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 >= 1
164
+ if token_available?(current_tokens, now, refill_rate)
165
165
  {
166
166
  allowed: true,
167
167
  retry_after: 0,
168
- tokens_remaining: (current_tokens - 1).floor
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
- local allow = (tat - now) <= delay_tolerance
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
- return { allow and 1 or 0, tat }
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
- local allow = (tat - now) <= delay_tolerance
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
- return { allow and 1 or 0, tat }
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
- local allow = tokens >= 1
16
- local tokens_after = allow and (tokens - 1) or 0
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
- return { allow and 1 or 0, tokens_after }
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
- local allow = tokens >= 1
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
- return { allow and 1 or 0, tokens }
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, current_time])
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
- allowed = result[0] == 1
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, current_time])
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
- allowed = result[0] == 1
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ThrottleMachines
4
- VERSION = '0.3.0'
4
+ VERSION = '0.3.1'
5
5
  end
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.0
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-12 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport