chrono_machines 0.10.0 → 0.12.0
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 +34 -0
- data/Cargo.lock +3 -3
- data/Cargo.toml +1 -1
- data/README.md +5 -3
- data/ext/chrono_machines_native/core/Cargo.toml +3 -3
- data/ext/chrono_machines_native/core/src/backoff.rs +20 -19
- data/ext/chrono_machines_native/core/src/retry.rs +13 -21
- data/ext/chrono_machines_native/ffi/Cargo.toml +2 -2
- data/ext/chrono_machines_native/ffi/src/lib.rs +1 -8
- data/lib/chrono_machines/executor.rb +40 -21
- data/lib/chrono_machines/native_speedup.rb +3 -8
- data/lib/chrono_machines/version.rb +1 -1
- data/lib/chrono_machines.rb +12 -0
- metadata +29 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6657b6db5e08ccde2f9145ccb5f2f5701ee4edd1c54a89998bfbe047510e024d
|
|
4
|
+
data.tar.gz: f34228d0ed9ce646a7d5eb4f180ff4c775f53bd9e799d33ec4cc80b861c27401
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c11744285959ebbaeb17cb77e141ea6c8e763b5dddce05a0de06cdcef7fd8ddedfa54a26f381a8435345490b0ad9ec1bae217463ed62bfcd795b05ac069384c
|
|
7
|
+
data.tar.gz: b4d6d6a1b519abba7b0d03c648cd4339782af0eda8d13e78e34232777105cb1023eef1d5040a08a57f41fec329d2dd587557578a466f79f04af8f519a8c6b868
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.12.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.11.0...chrono_machines/v0.12.0) (2026-11-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* `chrono_machines::fibonacci` returns f64, and `max_attempts` outside 1..255 is rejected instead of silently computed on one backend only.
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* expose full jitter as a one-shot ([ff78a9c](https://github.com/seuros/chrono_machines/commit/ff78a9ca3a9e3a3b3a64a5427290e9f72e55305a))
|
|
13
|
+
* honour an ambient deadline during retries ([97b24d0](https://github.com/seuros/chrono_machines/commit/97b24d0b73751a6a25f1ee703cb251ff43ed167a))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* a failing success callback no longer re-runs the operation ([920ecc9](https://github.com/seuros/chrono_machines/commit/920ecc9002b66a7617ed99537c63ab6b2238c1ba))
|
|
19
|
+
* a lone checkout can bundle ([19276ef](https://github.com/seuros/chrono_machines/commit/19276ef6b78f0a13c18c7d23e91e2830d9cf0b1c))
|
|
20
|
+
* a source install can build, and a failed gem push fails the release ([cd85f2c](https://github.com/seuros/chrono_machines/commit/cd85f2cc5270830a89b35e7217a10e2454813ab3))
|
|
21
|
+
* build the native speedup on CRuby only ([0b30fc6](https://github.com/seuros/chrono_machines/commit/0b30fc61b753908a9bff39e77b8854ab74ef603f))
|
|
22
|
+
* Fibonacci backoff no longer saturates on the native backend ([213413c](https://github.com/seuros/chrono_machines/commit/213413ce86af8107396d8462449e7cebd2430e17))
|
|
23
|
+
* load the native build tasks on CRuby only ([f0826ca](https://github.com/seuros/chrono_machines/commit/f0826ca5e9636facde86fb02fd30d5be28b683e0))
|
|
24
|
+
* ship the core's test files so the packaged crate builds ([89b1de5](https://github.com/seuros/chrono_machines/commit/89b1de5067b222de580d761ce17b439e8b689533))
|
|
25
|
+
|
|
26
|
+
## [0.11.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.10.0...chrono_machines/v0.11.0) (2026-10-14)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### ⚠ BREAKING CHANGES
|
|
30
|
+
|
|
31
|
+
* **core:** RetryError::cause() returns &E and into_cause() returns E instead of Option.
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
* **core:** RetryError always carries its cause ([e5222a3](https://github.com/seuros/chrono_machines/commit/e5222a3c394974ba86605944f02d820345cbae8e))
|
|
36
|
+
|
|
3
37
|
## [0.10.0](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.9.2...chrono_machines/v0.10.0) (2026-10-11)
|
|
4
38
|
|
|
5
39
|
|
data/Cargo.lock
CHANGED
|
@@ -69,7 +69,7 @@ dependencies = [
|
|
|
69
69
|
|
|
70
70
|
[[package]]
|
|
71
71
|
name = "chrono-machines"
|
|
72
|
-
version = "0.
|
|
72
|
+
version = "0.9.0"
|
|
73
73
|
dependencies = [
|
|
74
74
|
"rand",
|
|
75
75
|
"tokio",
|
|
@@ -233,9 +233,9 @@ checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
|
|
233
233
|
|
|
234
234
|
[[package]]
|
|
235
235
|
name = "magnus"
|
|
236
|
-
version = "0.9.
|
|
236
|
+
version = "0.9.3"
|
|
237
237
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
238
|
-
checksum = "
|
|
238
|
+
checksum = "40ef424404d166cbb71c8b8d19c9c589c748c4547c1afa1928c5504299f6c713"
|
|
239
239
|
dependencies = [
|
|
240
240
|
"magnus-macros",
|
|
241
241
|
"rb-sys",
|
data/Cargo.toml
CHANGED
|
@@ -8,7 +8,7 @@ rust-version = "1.99"
|
|
|
8
8
|
|
|
9
9
|
[workspace.dependencies]
|
|
10
10
|
chrono-machines = { path = "ext/chrono_machines_native/core" }
|
|
11
|
-
rand = { version = "0.10", default-features = false, features = ["std_rng"
|
|
11
|
+
rand = { version = "0.10", default-features = false, features = ["std_rng"] }
|
|
12
12
|
|
|
13
13
|
[workspace.lints.rust]
|
|
14
14
|
unsafe_code = "deny"
|
data/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Still here? Excellent. Because in the fabric of spacetime, nobody can hear your
|
|
|
39
39
|
|
|
40
40
|
### The Pattern Time Forgot
|
|
41
41
|
|
|
42
|
-
Built for Ruby
|
|
42
|
+
Built for Ruby 4.0+ with fiber-aware sleep and full jitter implementation, because when you're manipulating time itself, precision matters.
|
|
43
43
|
|
|
44
44
|
## Features
|
|
45
45
|
|
|
@@ -223,6 +223,8 @@ Anything else raises `ArgumentError` - a broken hook fails loudly instead of sil
|
|
|
223
223
|
|
|
224
224
|
### Scheduling retries without sleeping
|
|
225
225
|
|
|
226
|
+
See the [Rails integration guide](docs/RAILS_INTEGRATION.md) for Active Job patterns, policy callbacks, and safe retries.
|
|
227
|
+
|
|
226
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.
|
|
227
229
|
|
|
228
230
|
For Active Job's `retry_on`, keep the attempt limit in sync with the policy:
|
|
@@ -280,7 +282,7 @@ Jitter is drawn from the `random:` option (Ruby's `Random` by default) on both b
|
|
|
280
282
|
|
|
281
283
|
| Option | Default | Description |
|
|
282
284
|
|--------|---------|-------------|
|
|
283
|
-
| `max_attempts` | `3` | Maximum number of attempts, including the initial call |
|
|
285
|
+
| `max_attempts` | `3` | Maximum number of attempts, including the initial call; `1..255` |
|
|
284
286
|
| `base_delay` | `0.1` | Initial delay in seconds |
|
|
285
287
|
| `multiplier` | `2` | Exponential backoff multiplier |
|
|
286
288
|
| `max_delay` | `10` | Maximum delay cap in seconds |
|
|
@@ -625,7 +627,7 @@ end
|
|
|
625
627
|
## Why ChronoMachines?
|
|
626
628
|
|
|
627
629
|
### Built for Modern Ruby
|
|
628
|
-
- **Ruby
|
|
630
|
+
- **Ruby 4.0+ Support**: Fiber-aware sleep handling
|
|
629
631
|
- **Clean Architecture**: Separation of concerns with configurable policies
|
|
630
632
|
- **Rich Instrumentation**: Comprehensive callback system for monitoring
|
|
631
633
|
- **Battle-Tested**: Full jitter implementation prevents thundering herds
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "chrono-machines"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.0"
|
|
4
4
|
edition.workspace = true
|
|
5
5
|
rust-version.workspace = true
|
|
6
6
|
authors = ["Abdelkader Boudih <terminale@gmail.com>"]
|
|
@@ -10,7 +10,7 @@ repository = "https://github.com/seuros/chrono_machines"
|
|
|
10
10
|
keywords = ["retry", "backoff", "exponential", "jitter", "resilience"]
|
|
11
11
|
categories = ["no-std", "algorithms"]
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
exclude = ["tests/"
|
|
13
|
+
exclude = ["tests/"]
|
|
14
14
|
|
|
15
15
|
[lib]
|
|
16
16
|
name = "chrono_machines"
|
|
@@ -26,7 +26,7 @@ alloc = []
|
|
|
26
26
|
async = ["alloc"]
|
|
27
27
|
|
|
28
28
|
[dependencies]
|
|
29
|
-
rand = {
|
|
29
|
+
rand = { workspace = true }
|
|
30
30
|
|
|
31
31
|
[dev-dependencies]
|
|
32
32
|
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "time"] }
|
|
@@ -25,7 +25,7 @@ fn powi_f64(base: f64, exp: i32) -> f64 {
|
|
|
25
25
|
acc
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/// Widen a delay
|
|
28
|
+
/// Widen a delay to `f64` for the backoff arithmetic.
|
|
29
29
|
///
|
|
30
30
|
/// There is no lossless `u64 -> f64` conversion. Rounding only starts above
|
|
31
31
|
/// 2^53 (2^53 ms is ~285,000 years), so every delay that can actually be slept
|
|
@@ -53,23 +53,25 @@ pub(crate) fn exponential_ms(
|
|
|
53
53
|
base_exponential.min(lossy_f64(max_delay_ms))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
///
|
|
56
|
+
/// The nth Fibonacci number (1-indexed), as a delay factor: 1, 1, 2, 3, 5, 8, ...
|
|
57
|
+
///
|
|
58
|
+
/// The sequence passes `2^53` at n = 79 and `u64::MAX` at n = 94, so no integer
|
|
59
|
+
/// accumulator reaches the end of the `u8` attempt range. One that stops at its
|
|
60
|
+
/// own ceiling scales to a delay *under* `max_delay`, which is then silently
|
|
61
|
+
/// wrong rather than capped; the `f64` recurrence keeps growing and leaves the
|
|
62
|
+
/// decision to the cap. It rounds from n = 82 on, within two ulps of the exact
|
|
63
|
+
/// sequence over the whole range, and the gem's pure-Ruby backoff runs the same
|
|
64
|
+
/// recurrence, so the two backends agree bit for bit.
|
|
57
65
|
#[must_use]
|
|
58
|
-
pub fn fibonacci(n: u8) ->
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
a = b;
|
|
68
|
-
b = next;
|
|
69
|
-
}
|
|
70
|
-
b
|
|
71
|
-
}
|
|
72
|
-
}
|
|
66
|
+
pub fn fibonacci(n: u8) -> f64 {
|
|
67
|
+
let mut previous = 0.0_f64;
|
|
68
|
+
let mut current = 1.0_f64;
|
|
69
|
+
for _ in 0..n {
|
|
70
|
+
let next = previous + current;
|
|
71
|
+
previous = current;
|
|
72
|
+
current = next;
|
|
73
|
+
}
|
|
74
|
+
previous
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/// Blend a base delay with jitter using the full-jitter algorithm.
|
|
@@ -448,9 +450,8 @@ impl BackoffStrategy for FibonacciBackoff {
|
|
|
448
450
|
return None;
|
|
449
451
|
}
|
|
450
452
|
|
|
451
|
-
let fib = fibonacci(attempt);
|
|
452
453
|
let base =
|
|
453
|
-
(lossy_f64(self.base_delay_ms) *
|
|
454
|
+
(lossy_f64(self.base_delay_ms) * fibonacci(attempt)).min(lossy_f64(self.max_delay_ms));
|
|
454
455
|
|
|
455
456
|
Some(apply_jitter(base, self.jitter_factor, rng))
|
|
456
457
|
}
|
|
@@ -51,13 +51,7 @@ fn finalize_failure<E>(
|
|
|
51
51
|
cumulative_delay_ms: u64,
|
|
52
52
|
error: E,
|
|
53
53
|
) -> RetryError<E> {
|
|
54
|
-
let retry_error = RetryError::new(
|
|
55
|
-
kind,
|
|
56
|
-
attempt,
|
|
57
|
-
max_attempts,
|
|
58
|
-
cumulative_delay_ms,
|
|
59
|
-
Some(error),
|
|
60
|
-
);
|
|
54
|
+
let retry_error = RetryError::new(kind, attempt, max_attempts, cumulative_delay_ms, error);
|
|
61
55
|
if let Some(callback) = on_failure {
|
|
62
56
|
callback(&retry_error);
|
|
63
57
|
}
|
|
@@ -99,7 +93,7 @@ pub struct RetryError<E> {
|
|
|
99
93
|
attempts: u8,
|
|
100
94
|
max_attempts: u8,
|
|
101
95
|
cumulative_delay_ms: u64,
|
|
102
|
-
cause:
|
|
96
|
+
cause: E,
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
impl<E> RetryError<E> {
|
|
@@ -108,7 +102,7 @@ impl<E> RetryError<E> {
|
|
|
108
102
|
attempts: u8,
|
|
109
103
|
max_attempts: u8,
|
|
110
104
|
cumulative_delay_ms: u64,
|
|
111
|
-
cause:
|
|
105
|
+
cause: E,
|
|
112
106
|
) -> Self {
|
|
113
107
|
Self {
|
|
114
108
|
kind,
|
|
@@ -119,13 +113,13 @@ impl<E> RetryError<E> {
|
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
|
|
122
|
-
///
|
|
123
|
-
pub const fn cause(&self) ->
|
|
124
|
-
self.cause
|
|
116
|
+
/// The error from the final attempt.
|
|
117
|
+
pub const fn cause(&self) -> &E {
|
|
118
|
+
&self.cause
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
/// Consume the error and return the
|
|
128
|
-
pub fn into_cause(self) ->
|
|
121
|
+
/// Consume the error and return the error from the final attempt.
|
|
122
|
+
pub fn into_cause(self) -> E {
|
|
129
123
|
self.cause
|
|
130
124
|
}
|
|
131
125
|
|
|
@@ -175,13 +169,11 @@ where
|
|
|
175
169
|
}
|
|
176
170
|
}
|
|
177
171
|
|
|
178
|
-
write!(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Ok(())
|
|
172
|
+
write!(
|
|
173
|
+
f,
|
|
174
|
+
" (cumulative delay {}ms): {}",
|
|
175
|
+
self.cumulative_delay_ms, self.cause
|
|
176
|
+
)
|
|
185
177
|
}
|
|
186
178
|
}
|
|
187
179
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "chrono_machines_native"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.0"
|
|
4
4
|
edition.workspace = true
|
|
5
5
|
rust-version.workspace = true
|
|
6
6
|
authors = ["Abdelkader Boudih <terminale@gmail.com>"]
|
|
@@ -15,7 +15,7 @@ crate-type = ["cdylib"]
|
|
|
15
15
|
|
|
16
16
|
[dependencies]
|
|
17
17
|
chrono-machines = { workspace = true, features = ["std"] }
|
|
18
|
-
magnus = "0.9"
|
|
18
|
+
magnus = { version = ">= 0.9.3", default-features = false, features = ["io"] }
|
|
19
19
|
rand = { workspace = true }
|
|
20
20
|
rb-sys = { version = "0.9.124", default-features = false, features = ["stable-api-compiled-fallback"] }
|
|
21
21
|
|
|
@@ -73,14 +73,7 @@ fn calculate_delay_fibonacci(
|
|
|
73
73
|
) -> f64 {
|
|
74
74
|
let jitter_factor = normalize_jitter(jitter_factor);
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
// nearest f64, an error far below anything a delay in seconds can express.
|
|
78
|
-
#[expect(
|
|
79
|
-
clippy::cast_precision_loss,
|
|
80
|
-
reason = "u64 -> f64 has no lossless conversion; rounding only starts above 2^53"
|
|
81
|
-
)]
|
|
82
|
-
let fib = fibonacci(attempt_u8(attempt)) as f64;
|
|
83
|
-
let base = (base_delay * fib).min(max_delay);
|
|
76
|
+
let base = (base_delay * fibonacci(attempt_u8(attempt))).min(max_delay);
|
|
84
77
|
|
|
85
78
|
apply_jitter(base, jitter_factor)
|
|
86
79
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module ChronoMachines
|
|
4
4
|
class Executor
|
|
5
|
+
ATTEMPT_LIMIT = 255
|
|
6
|
+
|
|
5
7
|
def initialize(policy_or_options = {})
|
|
6
8
|
policy_options = if policy_or_options.is_a?(Symbol)
|
|
7
9
|
ChronoMachines.config.get_policy(policy_or_options)
|
|
@@ -11,6 +13,11 @@ module ChronoMachines
|
|
|
11
13
|
|
|
12
14
|
@backoff_strategy = policy_options[:backoff_strategy] || :exponential
|
|
13
15
|
@max_attempts = policy_options[:max_attempts]
|
|
16
|
+
unless @max_attempts.is_a?(Integer) && @max_attempts.between?(1, ATTEMPT_LIMIT)
|
|
17
|
+
raise ArgumentError, "max_attempts must be an Integer in 1..#{ATTEMPT_LIMIT} " \
|
|
18
|
+
"(the native backend counts attempts in a u8); got #{@max_attempts.inspect}"
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
@base_delay = policy_options[:base_delay]
|
|
15
22
|
@multiplier = policy_options[:multiplier] || 2
|
|
16
23
|
@max_delay = policy_options[:max_delay]
|
|
@@ -34,16 +41,14 @@ module ChronoMachines
|
|
|
34
41
|
|
|
35
42
|
def call
|
|
36
43
|
attempts = 0
|
|
44
|
+
DeadlineMachines.check!
|
|
37
45
|
|
|
38
|
-
begin
|
|
46
|
+
result = begin
|
|
39
47
|
attempts += 1
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# Call success callback if defined
|
|
43
|
-
@on_success&.call(result: result, attempts: attempts)
|
|
44
|
-
|
|
45
|
-
result
|
|
48
|
+
yield
|
|
46
49
|
rescue StandardError => e
|
|
50
|
+
raise if caller_gave_up?(e)
|
|
51
|
+
|
|
47
52
|
delay = next_delay(attempt: attempts, exception: e)
|
|
48
53
|
unless delay
|
|
49
54
|
handle_final_failure(e, attempts)
|
|
@@ -52,17 +57,33 @@ module ChronoMachines
|
|
|
52
57
|
raise e
|
|
53
58
|
end
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
stop_on_spent_budget(e, attempts)
|
|
61
|
+
budget = DeadlineMachines.clamp_sleep(delay)
|
|
62
|
+
@on_retry&.call(exception: e, attempt: attempts, next_delay: budget)
|
|
63
|
+
robust_sleep(DeadlineMachines.clamp_sleep(budget))
|
|
64
|
+
stop_on_spent_budget(e, attempts)
|
|
60
65
|
retry
|
|
61
66
|
end
|
|
67
|
+
|
|
68
|
+
@on_success&.call(result: result, attempts: attempts)
|
|
69
|
+
result
|
|
62
70
|
end
|
|
63
71
|
|
|
64
72
|
private
|
|
65
73
|
|
|
74
|
+
# A deadline error from a narrower inner scope is a timeout worth retrying.
|
|
75
|
+
# One raised because this caller's own budget is gone is not.
|
|
76
|
+
def caller_gave_up?(error)
|
|
77
|
+
error.is_a?(DeadlineMachines::Error) && DeadlineMachines.current&.done?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stop_on_spent_budget(exception, attempts)
|
|
81
|
+
return unless DeadlineMachines.current&.done?
|
|
82
|
+
|
|
83
|
+
handle_final_failure(exception, attempts)
|
|
84
|
+
DeadlineMachines.check!
|
|
85
|
+
end
|
|
86
|
+
|
|
66
87
|
def retryable_exception?(exception)
|
|
67
88
|
@retryable_exceptions.any? { |type| exception.is_a?(type) }
|
|
68
89
|
end
|
|
@@ -105,16 +126,14 @@ module ChronoMachines
|
|
|
105
126
|
apply_jitter(base_delay)
|
|
106
127
|
end
|
|
107
128
|
|
|
108
|
-
#
|
|
129
|
+
# The nth Fibonacci number (1-indexed), as a delay factor, accumulated step
|
|
130
|
+
# for step with `fibonacci` in ext/chrono_machines_native/core/src/backoff.rs,
|
|
131
|
+
# whose documentation covers why neither side counts in integers.
|
|
109
132
|
def fibonacci(n)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
(2...n).each do
|
|
115
|
-
a, b = b, a + b
|
|
116
|
-
end
|
|
117
|
-
b
|
|
133
|
+
previous = 0.0
|
|
134
|
+
current = 1.0
|
|
135
|
+
n.times { previous, current = current, previous + current }
|
|
136
|
+
previous
|
|
118
137
|
end
|
|
119
138
|
|
|
120
139
|
# Main calculate_delay dispatches to the appropriate strategy
|
|
@@ -5,14 +5,9 @@
|
|
|
5
5
|
# to use the native Rust implementation for delay calculations.
|
|
6
6
|
|
|
7
7
|
begin
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
if File.exist?("#{local_ext_path}.bundle") || File.exist?("#{local_ext_path}.so")
|
|
12
|
-
require local_ext_path
|
|
13
|
-
else
|
|
14
|
-
require 'chrono_machines_native/chrono_machines_native'
|
|
15
|
-
end
|
|
8
|
+
# `rake compile` installs the extension here. A leftover build under ext/
|
|
9
|
+
# must not shadow it: that is how a stale native backend gets tested.
|
|
10
|
+
require 'chrono_machines_native/chrono_machines_native'
|
|
16
11
|
|
|
17
12
|
module ChronoMachines
|
|
18
13
|
# Native speedup module that overrides backoff calculations
|
data/lib/chrono_machines.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'deadline_machines'
|
|
3
4
|
require 'zeitwerk'
|
|
4
5
|
|
|
5
6
|
loader = Zeitwerk::Loader.for_gem
|
|
@@ -27,6 +28,17 @@ module ChronoMachines
|
|
|
27
28
|
def self.retry(policy_name_or_options = {}, &)
|
|
28
29
|
Executor.new(policy_name_or_options).call(&)
|
|
29
30
|
end
|
|
31
|
+
|
|
32
|
+
# Full jitter over a single duration: the result lands in
|
|
33
|
+
# `seconds * (1 - factor)` to `seconds`, so a fleet of callers waiting the
|
|
34
|
+
# same nominal time does not wake up together.
|
|
35
|
+
def self.jitter(seconds, factor:, random: Random)
|
|
36
|
+
factor = Float(factor)
|
|
37
|
+
raise ArgumentError, 'factor cannot be NaN' if factor.nan?
|
|
38
|
+
|
|
39
|
+
factor = factor.clamp(0.0, 1.0)
|
|
40
|
+
seconds * (1 - factor + (random.rand * factor))
|
|
41
|
+
end
|
|
30
42
|
end
|
|
31
43
|
|
|
32
44
|
# Load optional async support after core is loaded
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chrono_machines
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -9,6 +9,34 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: deadline_machines
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rb_sys
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.9'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.9'
|
|
12
40
|
- !ruby/object:Gem::Dependency
|
|
13
41
|
name: zeitwerk
|
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|