chrono_machines 0.9.2-aarch64-linux → 0.11.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 +23 -0
- data/README.md +39 -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: fc22547860d54b36a7bc9c5c2afabc18840c94869a924a8318ea50036924f374
|
|
4
|
+
data.tar.gz: ab5a3d65ec8e91f04dfdb8eb0b6327bd5cbf5aba9be015f800be78a6c7d7e037
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0aa6eeb316ac2a1a415bc5e92682554bef7cedbcd52bca87b597ff8e32b1f1a7696954e0247c994e7dd3ab06435a1cb42c1fe0176641dc06e3146bc62027c27b
|
|
7
|
+
data.tar.gz: 26a71d24a664a8b19ece84ca8199c8200b4b5fa133820ae36fde090c05813eaaf5e9a7bf595aee17781c36546cdf556bccd669a84c30e16b3e4a5f2cac5fed23
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.11.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.10.0...chrono_machines/v0.11.0) (2026-10-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **core:** RetryError::cause() returns &E and into_cause() returns E instead of Option.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **core:** RetryError always carries its cause ([e5222a3](https://github.com/seuros/chrono_machines/commit/e5222a3c394974ba86605944f02d820345cbae8e))
|
|
13
|
+
|
|
14
|
+
## [0.10.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.2...chrono_machines/v0.10.0) (2026-10-11)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* 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))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* reject a jitter random source without #rand ([50734ee](https://github.com/seuros/chrono_machines/commit/50734eee35608d8a8324b38cf38f9ab1cc101c65))
|
|
25
|
+
|
|
3
26
|
## [0.9.2](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.1...chrono_machines/v0.9.2) (2026-10-10)
|
|
4
27
|
|
|
5
28
|
|
data/README.md
CHANGED
|
@@ -221,6 +221,41 @@ 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
|
+
See the [Rails integration guide](docs/RAILS_INTEGRATION.md) for Active Job patterns, policy callbacks, and safe retries.
|
|
227
|
+
|
|
228
|
+
`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.
|
|
229
|
+
|
|
230
|
+
For Active Job's `retry_on`, keep the attempt limit in sync with the policy:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
class FetchJob < ApplicationJob
|
|
234
|
+
RETRY_POLICY = ChronoMachines::Executor.new(max_attempts: 5, base_delay: 1)
|
|
235
|
+
|
|
236
|
+
retry_on IOError, attempts: 5,
|
|
237
|
+
wait: ->(executions) { RETRY_POLICY.next_delay(attempt: executions) }
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`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`.
|
|
242
|
+
|
|
243
|
+
To use exception hints with durable jobs, pass the attempt count to the next job:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
def perform(record_id, attempt: 1)
|
|
247
|
+
fetch_record(record_id)
|
|
248
|
+
rescue IOError => error
|
|
249
|
+
delay = RETRY_POLICY.next_delay(attempt: attempt, exception: error)
|
|
250
|
+
raise unless delay
|
|
251
|
+
|
|
252
|
+
self.class.set(wait_until: Time.current + delay)
|
|
253
|
+
.perform_later(record_id, attempt: attempt + 1)
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
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.
|
|
258
|
+
|
|
224
259
|
## The Science of Temporal Jitter
|
|
225
260
|
|
|
226
261
|
ChronoMachines implements **full jitter** exponential backoff:
|
|
@@ -239,16 +274,19 @@ ChronoMachines implements **full jitter** exponential backoff:
|
|
|
239
274
|
|
|
240
275
|
This prevents the "thundering herd" problem where multiple clients retry simultaneously, overwhelming recovering services.
|
|
241
276
|
|
|
277
|
+
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.
|
|
278
|
+
|
|
242
279
|
## Configuration Reference
|
|
243
280
|
|
|
244
281
|
### Policy Options
|
|
245
282
|
|
|
246
283
|
| Option | Default | Description |
|
|
247
284
|
|--------|---------|-------------|
|
|
248
|
-
| `max_attempts` | `3` | Maximum number of
|
|
285
|
+
| `max_attempts` | `3` | Maximum number of attempts, including the initial call |
|
|
249
286
|
| `base_delay` | `0.1` | Initial delay in seconds |
|
|
250
287
|
| `multiplier` | `2` | Exponential backoff multiplier |
|
|
251
288
|
| `max_delay` | `10` | Maximum delay cap in seconds |
|
|
289
|
+
| `random` | `Random` | Random source for jitter; accepts `Random.new(seed)` |
|
|
252
290
|
| `retryable_exceptions` | `[StandardError]` | Array of exception classes to retry |
|
|
253
291
|
| `on_success` | `nil` | Success callback: `(result:, attempts:)` |
|
|
254
292
|
| `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.11.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-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: zeitwerk
|