chrono_machines 0.9.1-aarch64-linux → 0.10.0-aarch64-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/CHANGELOG.md +24 -0
- data/README.md +37 -1
- data/lib/chrono_machines/executor.rb +31 -26
- data/lib/chrono_machines/native_speedup.rb +28 -26
- data/lib/chrono_machines/version.rb +1 -1
- data/lib/chrono_machines_native/chrono_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: aeef5fdf2ecf22b876dedb43acf7873ac34ba2ca05249490ab5b190048885ce6
|
|
4
|
+
data.tar.gz: 1256e269112ab4703288f2a5d44bd8f7183230a3b2f8e40278a1e756e9f55fcb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ad2fbc6e409420019e0589ed9d6b582a777c6d2e0a25bad4951d274af9c94df0811ae89b21558be6f3592ab69e8dfd0f7a0b59fe3115cbd14c85686eb131353
|
|
7
|
+
data.tar.gz: 9a64d22915c64d7a4a708abf18649a486338bbbcf5a58443ea4f11cc6e714e35758be3b2a8c6137d6c0432c8b7c0736d3fd02416e1fc861e2c0233ffd3246ee0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.10.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.2...chrono_machines/v0.10.0) (2026-10-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* expose retry delays without executing or sleeping ([#30](https://github.com/seuros/chrono_machines/issues/30)) ([2f6ce19](https://github.com/seuros/chrono_machines/commit/2f6ce1999c7d2056d3adc8e57e0025c22087ca01))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* reject a jitter random source without #rand ([50734ee](https://github.com/seuros/chrono_machines/commit/50734eee35608d8a8324b38cf38f9ab1cc101c65))
|
|
14
|
+
|
|
15
|
+
## [0.9.2](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.1...chrono_machines/v0.9.2) (2026-10-10)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **ci:** attach release gems with gh release upload ([87466ea](https://github.com/seuros/chrono_machines/commit/87466eafe0126e5473ff49f176180eae08337657))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Performance Improvements
|
|
24
|
+
|
|
25
|
+
* allocate outside the policy registry lock; adopt clippy pedantic and nursery ([80b40d8](https://github.com/seuros/chrono_machines/commit/80b40d87e5869c74b356a71425639ebf560cec4e))
|
|
26
|
+
|
|
3
27
|
## [0.9.1](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.0...chrono_machines/v0.9.1) (2026-10-05)
|
|
4
28
|
|
|
5
29
|
|
data/README.md
CHANGED
|
@@ -221,6 +221,39 @@ The hook's return value controls the retry:
|
|
|
221
221
|
|
|
222
222
|
Anything else raises `ArgumentError` - a broken hook fails loudly instead of silently mangling your retry cadence.
|
|
223
223
|
|
|
224
|
+
### Scheduling retries without sleeping
|
|
225
|
+
|
|
226
|
+
`Executor#next_delay(attempt:, exception: nil)` returns seconds to wait, or `nil` to stop. `attempt: 1` means the initial call has failed; `max_attempts` includes that initial call. It uses the same backoff, jitter, exception rules, and `delay_from` hints as `call`, without executing work, sleeping, or running lifecycle callbacks. The caller owns the attempt count and scheduling. Without an exception, `delay_from` and exception filtering are skipped.
|
|
227
|
+
|
|
228
|
+
For Active Job's `retry_on`, keep the attempt limit in sync with the policy:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
class FetchJob < ApplicationJob
|
|
232
|
+
RETRY_POLICY = ChronoMachines::Executor.new(max_attempts: 5, base_delay: 1)
|
|
233
|
+
|
|
234
|
+
retry_on IOError, attempts: 5,
|
|
235
|
+
wait: ->(executions) { RETRY_POLICY.next_delay(attempt: executions) }
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`retry_on` calls `wait` only while `executions < attempts`. If the two limits drift apart, `next_delay` returns `nil` and Active Job retries immediately, so keep `attempts:` equal to `max_attempts`.
|
|
240
|
+
|
|
241
|
+
To use exception hints with durable jobs, pass the attempt count to the next job:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
def perform(record_id, attempt: 1)
|
|
245
|
+
fetch_record(record_id)
|
|
246
|
+
rescue IOError => error
|
|
247
|
+
delay = RETRY_POLICY.next_delay(attempt: attempt, exception: error)
|
|
248
|
+
raise unless delay
|
|
249
|
+
|
|
250
|
+
self.class.set(wait_until: Time.current + delay)
|
|
251
|
+
.perform_later(record_id, attempt: attempt + 1)
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A hint beyond `max_delay` returns `nil`; it is never shortened to the cap. Use `random: Random.new(seed)` for deterministic jitter in tests, with either the Ruby or native backend.
|
|
256
|
+
|
|
224
257
|
## The Science of Temporal Jitter
|
|
225
258
|
|
|
226
259
|
ChronoMachines implements **full jitter** exponential backoff:
|
|
@@ -239,16 +272,19 @@ ChronoMachines implements **full jitter** exponential backoff:
|
|
|
239
272
|
|
|
240
273
|
This prevents the "thundering herd" problem where multiple clients retry simultaneously, overwhelming recovering services.
|
|
241
274
|
|
|
275
|
+
Jitter is drawn from the `random:` option (Ruby's `Random` by default) on both backends; the native extension computes only the un-jittered base delay.
|
|
276
|
+
|
|
242
277
|
## Configuration Reference
|
|
243
278
|
|
|
244
279
|
### Policy Options
|
|
245
280
|
|
|
246
281
|
| Option | Default | Description |
|
|
247
282
|
|--------|---------|-------------|
|
|
248
|
-
| `max_attempts` | `3` | Maximum number of
|
|
283
|
+
| `max_attempts` | `3` | Maximum number of attempts, including the initial call |
|
|
249
284
|
| `base_delay` | `0.1` | Initial delay in seconds |
|
|
250
285
|
| `multiplier` | `2` | Exponential backoff multiplier |
|
|
251
286
|
| `max_delay` | `10` | Maximum delay cap in seconds |
|
|
287
|
+
| `random` | `Random` | Random source for jitter; accepts `Random.new(seed)` |
|
|
252
288
|
| `retryable_exceptions` | `[StandardError]` | Array of exception classes to retry |
|
|
253
289
|
| `on_success` | `nil` | Success callback: `(result:, attempts:)` |
|
|
254
290
|
| `on_retry` | `nil` | Retry callback: `(exception:, attempt:, next_delay:)` |
|
|
@@ -15,6 +15,8 @@ module ChronoMachines
|
|
|
15
15
|
@multiplier = policy_options[:multiplier] || 2
|
|
16
16
|
@max_delay = policy_options[:max_delay]
|
|
17
17
|
@jitter_factor = policy_options[:jitter_factor]
|
|
18
|
+
@random = policy_options[:random] || Random
|
|
19
|
+
raise ArgumentError, "random must respond to #rand; got #{@random.inspect}" unless @random.respond_to?(:rand)
|
|
18
20
|
@retryable_exceptions = policy_options[:retryable_exceptions]
|
|
19
21
|
@delay_from = policy_options[:delay_from]
|
|
20
22
|
@on_failure = policy_options[:on_failure]
|
|
@@ -22,6 +24,14 @@ module ChronoMachines
|
|
|
22
24
|
@on_success = policy_options[:on_success]
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
def next_delay(attempt:, exception: nil)
|
|
28
|
+
raise ArgumentError, 'attempt must be a positive Integer' unless attempt.is_a?(Integer) && attempt.positive?
|
|
29
|
+
return if exception && !retryable_exception?(exception)
|
|
30
|
+
return if attempt >= @max_attempts
|
|
31
|
+
|
|
32
|
+
resolve_delay(exception, attempt)
|
|
33
|
+
end
|
|
34
|
+
|
|
25
35
|
def call
|
|
26
36
|
attempts = 0
|
|
27
37
|
|
|
@@ -34,22 +44,14 @@ module ChronoMachines
|
|
|
34
44
|
|
|
35
45
|
result
|
|
36
46
|
rescue StandardError => e
|
|
37
|
-
|
|
38
|
-
unless
|
|
39
|
-
# Non-retryable exception - call failure callback and re-raise
|
|
47
|
+
delay = next_delay(attempt: attempts, exception: e)
|
|
48
|
+
unless delay
|
|
40
49
|
handle_final_failure(e, attempts)
|
|
41
|
-
raise e
|
|
42
|
-
end
|
|
50
|
+
raise MaxRetriesExceededError.new(e, attempts) if retryable_exception?(e) && attempts >= @max_attempts
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
if attempts >= @max_attempts
|
|
46
|
-
handle_final_failure(e, attempts)
|
|
47
|
-
raise MaxRetriesExceededError.new(e, attempts)
|
|
52
|
+
raise e
|
|
48
53
|
end
|
|
49
54
|
|
|
50
|
-
# Calculate delay
|
|
51
|
-
delay = resolve_delay(e, attempts)
|
|
52
|
-
|
|
53
55
|
# Call retry callback if defined
|
|
54
56
|
@on_retry&.call(exception: e, attempt: attempts, next_delay: delay)
|
|
55
57
|
|
|
@@ -61,24 +63,25 @@ module ChronoMachines
|
|
|
61
63
|
|
|
62
64
|
private
|
|
63
65
|
|
|
66
|
+
def retryable_exception?(exception)
|
|
67
|
+
@retryable_exceptions.any? { |type| exception.is_a?(type) }
|
|
68
|
+
end
|
|
69
|
+
|
|
64
70
|
# delay_from(exception:, attempt:) overrides the backoff: nil -> backoff,
|
|
65
|
-
# Numeric -> verbatim (beyond max_delay: halt), :halt/false ->
|
|
71
|
+
# Numeric -> verbatim (beyond max_delay: halt), :halt/false -> stop.
|
|
66
72
|
def resolve_delay(exception, attempts)
|
|
67
|
-
return calculate_delay(attempts) unless @delay_from
|
|
73
|
+
return calculate_delay(attempts) unless exception && @delay_from
|
|
68
74
|
|
|
69
75
|
hint = @delay_from.call(exception: exception, attempt: attempts)
|
|
70
76
|
case hint
|
|
71
77
|
when nil
|
|
72
78
|
calculate_delay(attempts)
|
|
73
79
|
when Numeric
|
|
74
|
-
if @max_delay && hint > @max_delay
|
|
75
|
-
|
|
76
|
-
raise exception
|
|
77
|
-
end
|
|
80
|
+
return if @max_delay && hint > @max_delay
|
|
81
|
+
|
|
78
82
|
hint
|
|
79
83
|
when :halt, false
|
|
80
|
-
|
|
81
|
-
raise exception
|
|
84
|
+
nil
|
|
82
85
|
else
|
|
83
86
|
raise ArgumentError, "delay_from must return nil, a Numeric, :halt or false; got #{hint.inspect}"
|
|
84
87
|
end
|
|
@@ -87,22 +90,19 @@ module ChronoMachines
|
|
|
87
90
|
# Pure Ruby implementation of delay calculation (exponential backoff)
|
|
88
91
|
def ruby_calculate_delay_exponential(attempts)
|
|
89
92
|
base_exponential_delay = [@base_delay * (@multiplier**(attempts - 1)), @max_delay].min
|
|
90
|
-
|
|
91
|
-
base_exponential_delay * (1 - jitter_factor + (rand * jitter_factor))
|
|
93
|
+
apply_jitter(base_exponential_delay)
|
|
92
94
|
end
|
|
93
95
|
|
|
94
96
|
# Pure Ruby implementation of constant backoff
|
|
95
97
|
def ruby_calculate_delay_constant(_attempts)
|
|
96
|
-
|
|
97
|
-
@base_delay * (1 - jitter_factor + (rand * jitter_factor))
|
|
98
|
+
apply_jitter(@base_delay)
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
# Pure Ruby implementation of Fibonacci backoff
|
|
101
102
|
def ruby_calculate_delay_fibonacci(attempts)
|
|
102
103
|
fib = fibonacci(attempts)
|
|
103
104
|
base_delay = [@base_delay * fib, @max_delay].min
|
|
104
|
-
|
|
105
|
-
base_delay * (1 - jitter_factor + (rand * jitter_factor))
|
|
105
|
+
apply_jitter(base_delay)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
# Calculate Fibonacci number (1-indexed)
|
|
@@ -134,6 +134,11 @@ module ChronoMachines
|
|
|
134
134
|
# By default, use Ruby implementation (may be overridden by native extension)
|
|
135
135
|
alias_method :calculate_delay, :ruby_calculate_delay
|
|
136
136
|
|
|
137
|
+
def apply_jitter(delay)
|
|
138
|
+
jitter_factor = normalized_jitter_factor
|
|
139
|
+
delay * (1 - jitter_factor + (@random.rand * jitter_factor))
|
|
140
|
+
end
|
|
141
|
+
|
|
137
142
|
def robust_sleep(delay)
|
|
138
143
|
# Handle potential interruptions to sleep
|
|
139
144
|
# In Ruby 3.2+, Kernel.sleep is fiber-aware
|
|
@@ -22,32 +22,34 @@ begin
|
|
|
22
22
|
|
|
23
23
|
# Override calculate_delay to dispatch to native implementations
|
|
24
24
|
def calculate_delay(attempts)
|
|
25
|
-
case @backoff_strategy
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
delay = case @backoff_strategy
|
|
26
|
+
when :exponential
|
|
27
|
+
ChronoMachinesNative.exponential_delay(
|
|
28
|
+
attempts,
|
|
29
|
+
@base_delay,
|
|
30
|
+
@multiplier,
|
|
31
|
+
@max_delay,
|
|
32
|
+
0.0
|
|
33
|
+
)
|
|
34
|
+
when :constant
|
|
35
|
+
ChronoMachinesNative.constant_delay(
|
|
36
|
+
attempts,
|
|
37
|
+
@base_delay,
|
|
38
|
+
0.0
|
|
39
|
+
)
|
|
40
|
+
when :fibonacci
|
|
41
|
+
ChronoMachinesNative.fibonacci_delay(
|
|
42
|
+
attempts,
|
|
43
|
+
@base_delay,
|
|
44
|
+
@max_delay,
|
|
45
|
+
0.0
|
|
46
|
+
)
|
|
47
|
+
else
|
|
48
|
+
# Unknown strategy, fall back to Ruby
|
|
49
|
+
return super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
apply_jitter(delay)
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
|
|
Binary file
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chrono_machines
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: aarch64-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-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: zeitwerk
|