chrono_machines 0.5.2 → 0.9.1
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 +86 -0
- data/Cargo.lock +10 -22
- data/README.md +26 -0
- data/ext/chrono_machines_native/core/Cargo.toml +7 -4
- data/ext/chrono_machines_native/core/src/backoff.rs +58 -92
- data/ext/chrono_machines_native/core/src/dsl.rs +1 -38
- data/ext/chrono_machines_native/core/src/lib.rs +36 -149
- data/ext/chrono_machines_native/core/src/retry.rs +340 -905
- data/ext/chrono_machines_native/core/src/sleep.rs +65 -31
- data/ext/chrono_machines_native/ffi/Cargo.toml +3 -3
- data/ext/chrono_machines_native/ffi/src/lib.rs +8 -68
- data/lib/chrono_machines/configuration.rb +1 -0
- data/lib/chrono_machines/executor.rb +25 -1
- data/lib/chrono_machines/version.rb +1 -1
- metadata +3 -3
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
//! Sleep abstraction for no_std compatibility
|
|
2
2
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
3
|
+
//! Two traits live here, and which one you want is decided by your runtime:
|
|
4
|
+
//!
|
|
5
|
+
//! - [`Sleeper`] blocks the calling thread. Use it from sync code and embedded.
|
|
6
|
+
//! - [`AsyncSleeper`] yields to a reactor (`async` feature). Use it from tokio,
|
|
7
|
+
//! async-std, embassy or any other executor.
|
|
8
|
+
//!
|
|
9
|
+
//! They are separate traits rather than one trait with two methods because a
|
|
10
|
+
//! blocking sleep cannot be adapted into an async one: `block_on` inside a
|
|
11
|
+
//! runtime worker stalls that worker, and deadlocks outright on a
|
|
12
|
+
//! current-thread runtime.
|
|
5
13
|
|
|
6
|
-
/// Trait for sleep/delay implementations
|
|
14
|
+
/// Trait for blocking sleep/delay implementations
|
|
7
15
|
///
|
|
8
16
|
/// This trait abstracts sleep operations to support different runtime environments:
|
|
9
17
|
/// - Standard library blocking sleep
|
|
10
|
-
/// -
|
|
11
|
-
/// - Embassy timer for embedded
|
|
18
|
+
/// - Embassy/HAL busy-wait or timer delays for embedded
|
|
12
19
|
/// - Custom implementations
|
|
20
|
+
///
|
|
21
|
+
/// Async runtimes want [`AsyncSleeper`] instead.
|
|
13
22
|
pub trait Sleeper {
|
|
14
23
|
/// Sleep for the specified number of milliseconds
|
|
15
24
|
fn sleep_ms(&self, ms: u64);
|
|
@@ -42,7 +51,7 @@ impl Sleeper for StdSleeper {
|
|
|
42
51
|
/// Function pointer sleeper for custom sleep implementations
|
|
43
52
|
///
|
|
44
53
|
/// Wraps a function pointer that takes milliseconds and performs sleep.
|
|
45
|
-
/// Useful for
|
|
54
|
+
/// Useful for embedded HAL delays or testing.
|
|
46
55
|
///
|
|
47
56
|
/// # Example
|
|
48
57
|
///
|
|
@@ -67,31 +76,56 @@ impl Sleeper for FnSleeper {
|
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
/// Trait for asynchronous sleep/delay implementations
|
|
80
|
+
///
|
|
81
|
+
/// The async counterpart to [`Sleeper`], used by
|
|
82
|
+
/// [`RetryBuilder::call_async`](crate::RetryBuilder::call_async). It is defined
|
|
83
|
+
/// with an `async fn` in trait rather than a boxed future, so the returned
|
|
84
|
+
/// future stays a concrete type and the crate needs no `async-trait`
|
|
85
|
+
/// dependency and performs no allocation per sleep.
|
|
86
|
+
///
|
|
87
|
+
/// Any `Fn(u64) -> Future<Output = ()>` implements it, which covers every
|
|
88
|
+
/// runtime without this crate depending on one:
|
|
89
|
+
///
|
|
90
|
+
/// ```rust,ignore
|
|
91
|
+
/// // tokio
|
|
92
|
+
/// let sleeper = |ms| tokio::time::sleep(Duration::from_millis(ms));
|
|
93
|
+
///
|
|
94
|
+
/// // embassy
|
|
95
|
+
/// let sleeper = |ms| embassy_time::Timer::after_millis(ms);
|
|
96
|
+
/// ```
|
|
97
|
+
///
|
|
98
|
+
/// # `Send` futures
|
|
99
|
+
///
|
|
100
|
+
/// The trait declares no `Send` bound, so it stays usable on
|
|
101
|
+
/// single-threaded executors. Send-ness is not lost, though: it leaks through
|
|
102
|
+
/// auto-trait inference at the concrete instantiation, so a retry future built
|
|
103
|
+
/// from a `Send` sleeper and a `Send` operation is itself `Send` and can be
|
|
104
|
+
/// handed to `tokio::spawn`. Only code that is *itself generic* over
|
|
105
|
+
/// `S: AsyncSleeper` and must promise `Send` needs to spell the bound out, via
|
|
106
|
+
/// return-type notation:
|
|
107
|
+
///
|
|
108
|
+
/// ```rust,ignore
|
|
109
|
+
/// async fn spawnable<S>(sleeper: S)
|
|
110
|
+
/// where
|
|
111
|
+
/// S: AsyncSleeper<sleep_ms(..): Send> + Send + 'static,
|
|
112
|
+
/// ```
|
|
113
|
+
#[cfg(feature = "async")]
|
|
114
|
+
pub trait AsyncSleeper {
|
|
115
|
+
/// Sleep for the specified number of milliseconds, yielding to the executor
|
|
116
|
+
fn sleep_ms(&self, ms: u64) -> impl core::future::Future<Output = ()>;
|
|
117
|
+
}
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
#[cfg(feature = "async")]
|
|
120
|
+
impl<F, Fut> AsyncSleeper for F
|
|
121
|
+
where
|
|
122
|
+
F: Fn(u64) -> Fut,
|
|
123
|
+
Fut: core::future::Future<Output = ()>,
|
|
124
|
+
{
|
|
125
|
+
fn sleep_ms(&self, ms: u64) -> impl core::future::Future<Output = ()> {
|
|
126
|
+
self(ms)
|
|
96
127
|
}
|
|
97
128
|
}
|
|
129
|
+
|
|
130
|
+
#[cfg(test)]
|
|
131
|
+
mod tests;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "chrono_machines_native"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
authors = ["Abdelkader Boudih <terminale@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -14,9 +14,9 @@ crate-type = ["cdylib"]
|
|
|
14
14
|
|
|
15
15
|
[dependencies]
|
|
16
16
|
chrono-machines = { workspace = true, features = ["std"] }
|
|
17
|
-
magnus = "0.
|
|
17
|
+
magnus = "0.9"
|
|
18
18
|
rand = { workspace = true }
|
|
19
|
-
rb-sys = { version = "0.9.
|
|
19
|
+
rb-sys = { version = "0.9.124", default-features = false, features = ["stable-api-compiled-fallback"] }
|
|
20
20
|
|
|
21
21
|
[features]
|
|
22
22
|
default = ["std"]
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
#![warn(clippy::all)]
|
|
8
8
|
|
|
9
9
|
use chrono_machines::fibonacci;
|
|
10
|
-
use magnus::{
|
|
11
|
-
use rand::rngs::StdRng;
|
|
10
|
+
use magnus::{Error, Ruby, function};
|
|
12
11
|
use rand::RngExt;
|
|
12
|
+
use rand::rngs::StdRng;
|
|
13
13
|
use std::cell::RefCell;
|
|
14
14
|
|
|
15
15
|
// Thread-local RNG for performance (avoids reseeding from entropy on every call)
|
|
@@ -106,6 +106,11 @@ fn apply_jitter(base: f64, jitter_factor: f64) -> f64 {
|
|
|
106
106
|
/// Initialize the Ruby extension
|
|
107
107
|
#[magnus::init]
|
|
108
108
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
109
|
+
// Must precede every method definition: Ruby marks methods Ractor-safe as
|
|
110
|
+
// they are defined. The RNG is thread-local, so Ractors never share it.
|
|
111
|
+
// SAFETY: called on the loading thread during extension initialisation.
|
|
112
|
+
unsafe { rb_sys::rb_ext_ractor_safe(true) };
|
|
113
|
+
|
|
109
114
|
// Create ChronoMachinesNative module
|
|
110
115
|
let module = ruby.define_module("ChronoMachinesNative")?;
|
|
111
116
|
|
|
@@ -124,69 +129,4 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
#[cfg(test)]
|
|
127
|
-
mod tests
|
|
128
|
-
use super::*;
|
|
129
|
-
use rand::SeedableRng;
|
|
130
|
-
|
|
131
|
-
#[test]
|
|
132
|
-
fn test_exponential_delay() {
|
|
133
|
-
RNG.with(|rng| {
|
|
134
|
-
*rng.borrow_mut() = StdRng::seed_from_u64(1337);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
let delay = calculate_delay_exponential(1, 0.0004, 1.0, 0.001, 0.5);
|
|
138
|
-
|
|
139
|
-
assert!(
|
|
140
|
-
delay >= 0.0002 && delay <= 0.0004,
|
|
141
|
-
"expected delay in [0.0002, 0.0004], got {delay}"
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
#[test]
|
|
146
|
-
fn test_constant_delay() {
|
|
147
|
-
RNG.with(|rng| {
|
|
148
|
-
*rng.borrow_mut() = StdRng::seed_from_u64(42);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Constant delay with 10% jitter should be 90-100% of base
|
|
152
|
-
let delay = calculate_delay_constant(5, 1.0, 0.1);
|
|
153
|
-
assert!(delay >= 0.9 && delay <= 1.0, "got {delay}");
|
|
154
|
-
|
|
155
|
-
// No jitter should return exact value
|
|
156
|
-
let delay = calculate_delay_constant(3, 0.5, 0.0);
|
|
157
|
-
assert_eq!(delay, 0.5);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
#[test]
|
|
161
|
-
fn test_fibonacci_delay() {
|
|
162
|
-
RNG.with(|rng| {
|
|
163
|
-
*rng.borrow_mut() = StdRng::seed_from_u64(123);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Fibonacci sequence: 1, 1, 2, 3, 5, 8, 13...
|
|
167
|
-
// Attempt 1: base_delay * 1 = 0.1
|
|
168
|
-
let delay1 = calculate_delay_fibonacci(1, 0.1, 10.0, 0.0);
|
|
169
|
-
assert_eq!(delay1, 0.1);
|
|
170
|
-
|
|
171
|
-
// Attempt 5: base_delay * 5 = 0.5
|
|
172
|
-
let delay5 = calculate_delay_fibonacci(5, 0.1, 10.0, 0.0);
|
|
173
|
-
assert_eq!(delay5, 0.5);
|
|
174
|
-
|
|
175
|
-
// Attempt 8: base_delay * 21 = 2.1
|
|
176
|
-
let delay8 = calculate_delay_fibonacci(8, 0.1, 10.0, 0.0);
|
|
177
|
-
assert_eq!(delay8, 2.1);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
#[test]
|
|
181
|
-
fn test_fibonacci_sequence() {
|
|
182
|
-
assert_eq!(fibonacci(0), 0);
|
|
183
|
-
assert_eq!(fibonacci(1), 1);
|
|
184
|
-
assert_eq!(fibonacci(2), 1);
|
|
185
|
-
assert_eq!(fibonacci(3), 2);
|
|
186
|
-
assert_eq!(fibonacci(4), 3);
|
|
187
|
-
assert_eq!(fibonacci(5), 5);
|
|
188
|
-
assert_eq!(fibonacci(6), 8);
|
|
189
|
-
assert_eq!(fibonacci(7), 13);
|
|
190
|
-
assert_eq!(fibonacci(8), 21);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
132
|
+
mod tests;
|
|
@@ -18,6 +18,7 @@ module ChronoMachines
|
|
|
18
18
|
max_delay: 10, # seconds
|
|
19
19
|
jitter_factor: 1.0, # 1.0 = full jitter (recommended), 0.0 = no jitter
|
|
20
20
|
retryable_exceptions: [StandardError],
|
|
21
|
+
delay_from: nil, # Optional delay override from the exception (e.g. HTTP Retry-After)
|
|
21
22
|
on_failure: nil, # Fallback block when all retries are exhausted
|
|
22
23
|
on_retry: nil, # Callback block when a retry occurs
|
|
23
24
|
on_success: nil # Callback block when operation succeeds
|
|
@@ -16,6 +16,7 @@ module ChronoMachines
|
|
|
16
16
|
@max_delay = policy_options[:max_delay]
|
|
17
17
|
@jitter_factor = policy_options[:jitter_factor]
|
|
18
18
|
@retryable_exceptions = policy_options[:retryable_exceptions]
|
|
19
|
+
@delay_from = policy_options[:delay_from]
|
|
19
20
|
@on_failure = policy_options[:on_failure]
|
|
20
21
|
@on_retry = policy_options[:on_retry]
|
|
21
22
|
@on_success = policy_options[:on_success]
|
|
@@ -47,7 +48,7 @@ module ChronoMachines
|
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# Calculate delay
|
|
50
|
-
delay =
|
|
51
|
+
delay = resolve_delay(e, attempts)
|
|
51
52
|
|
|
52
53
|
# Call retry callback if defined
|
|
53
54
|
@on_retry&.call(exception: e, attempt: attempts, next_delay: delay)
|
|
@@ -60,6 +61,29 @@ module ChronoMachines
|
|
|
60
61
|
|
|
61
62
|
private
|
|
62
63
|
|
|
64
|
+
# delay_from(exception:, attempt:) overrides the backoff: nil -> backoff,
|
|
65
|
+
# Numeric -> verbatim (beyond max_delay: halt), :halt/false -> re-raise.
|
|
66
|
+
def resolve_delay(exception, attempts)
|
|
67
|
+
return calculate_delay(attempts) unless @delay_from
|
|
68
|
+
|
|
69
|
+
hint = @delay_from.call(exception: exception, attempt: attempts)
|
|
70
|
+
case hint
|
|
71
|
+
when nil
|
|
72
|
+
calculate_delay(attempts)
|
|
73
|
+
when Numeric
|
|
74
|
+
if @max_delay && hint > @max_delay
|
|
75
|
+
handle_final_failure(exception, attempts)
|
|
76
|
+
raise exception
|
|
77
|
+
end
|
|
78
|
+
hint
|
|
79
|
+
when :halt, false
|
|
80
|
+
handle_final_failure(exception, attempts)
|
|
81
|
+
raise exception
|
|
82
|
+
else
|
|
83
|
+
raise ArgumentError, "delay_from must return nil, a Numeric, :halt or false; got #{hint.inspect}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
63
87
|
# Pure Ruby implementation of delay calculation (exponential backoff)
|
|
64
88
|
def ruby_calculate_delay_exponential(attempts)
|
|
65
89
|
base_exponential_delay = [@base_delay * (@multiplier**(attempts - 1)), @max_delay].min
|
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.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -79,14 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
79
79
|
requirements:
|
|
80
80
|
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version:
|
|
82
|
+
version: 4.0.0
|
|
83
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
84
|
requirements:
|
|
85
85
|
- - ">="
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
87
|
version: '0'
|
|
88
88
|
requirements: []
|
|
89
|
-
rubygems_version:
|
|
89
|
+
rubygems_version: 4.0.20
|
|
90
90
|
specification_version: 4
|
|
91
91
|
summary: A robust Ruby gem for implementing retry mechanisms with exponential backoff
|
|
92
92
|
and jitter.
|