chrono_machines 0.2.0 → 0.5.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 +67 -0
- data/README.md +1 -1
- data/ext/chrono_machines_native/core/Cargo.toml +28 -0
- data/ext/chrono_machines_native/core/src/backoff.rs +516 -0
- data/ext/chrono_machines_native/core/src/dsl.rs +108 -0
- data/ext/chrono_machines_native/core/src/lib.rs +355 -0
- data/ext/chrono_machines_native/core/src/policy.rs +189 -0
- data/ext/chrono_machines_native/core/src/retry.rs +1511 -0
- data/ext/chrono_machines_native/core/src/sleep.rs +97 -0
- data/ext/chrono_machines_native/extconf.rb +3 -0
- data/ext/chrono_machines_native/ffi/Cargo.toml +23 -0
- data/ext/chrono_machines_native/ffi/extconf.rb +62 -0
- data/ext/chrono_machines_native/ffi/src/lib.rs +192 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/common.rs +355 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/dynamic.rs +276 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/macros.rs +49 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/common.rs +355 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/dynamic.rs +276 -0
- data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/macros.rs +49 -0
- data/ext/chrono_machines_native/target/debug/build/rb-sys-7d416123f8551038/out/bindings-0.9.124-mri-arm64-darwin23-4.0.3.rs +9057 -0
- data/ext/chrono_machines_native/target/debug/build/rb-sys-848783778984ec5d/out/bindings-0.9.124-mri-arm64-darwin23-3.4.8.rs +8933 -0
- data/ext/chrono_machines_native/target/debug/build/rb-sys-ce17442a66050c01/out/bindings-0.9.128-mri-arm64-darwin23-3.4.8.rs +8933 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/Cargo.toml +76 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/backoff.rs +497 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/dsl.rs +108 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/lib.rs +352 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/policy.rs +189 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/retry.rs +1489 -0
- data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/sleep.rs +97 -0
- data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/Cargo.toml +56 -0
- data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/src/lib.rs +192 -0
- data/lib/chrono_machines/async_support.rb +2 -1
- data/lib/chrono_machines/configuration.rb +19 -5
- data/lib/chrono_machines/executor.rb +69 -9
- data/lib/chrono_machines/native_speedup.rb +61 -0
- data/lib/chrono_machines/version.rb +1 -1
- data/lib/chrono_machines.rb +1 -1
- metadata +40 -7
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//! Sleep abstraction for no_std compatibility
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides traits and implementations for sleeping/delaying
|
|
4
|
+
//! in various environments (std, async, embedded).
|
|
5
|
+
|
|
6
|
+
/// Trait for sleep/delay implementations
|
|
7
|
+
///
|
|
8
|
+
/// This trait abstracts sleep operations to support different runtime environments:
|
|
9
|
+
/// - Standard library blocking sleep
|
|
10
|
+
/// - Tokio/async-std async sleep
|
|
11
|
+
/// - Embassy timer for embedded
|
|
12
|
+
/// - Custom implementations
|
|
13
|
+
pub trait Sleeper {
|
|
14
|
+
/// Sleep for the specified number of milliseconds
|
|
15
|
+
fn sleep_ms(&self, ms: u64);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Standard library sleeper using `std::thread::sleep`
|
|
19
|
+
///
|
|
20
|
+
/// Only available when the `std` feature is enabled.
|
|
21
|
+
///
|
|
22
|
+
/// # Example
|
|
23
|
+
///
|
|
24
|
+
/// ```rust
|
|
25
|
+
/// use chrono_machines::sleep::StdSleeper;
|
|
26
|
+
/// use chrono_machines::sleep::Sleeper;
|
|
27
|
+
///
|
|
28
|
+
/// let sleeper = StdSleeper;
|
|
29
|
+
/// sleeper.sleep_ms(100); // Sleep for 100ms
|
|
30
|
+
/// ```
|
|
31
|
+
#[cfg(feature = "std")]
|
|
32
|
+
#[derive(Debug, Clone, Copy)]
|
|
33
|
+
pub struct StdSleeper;
|
|
34
|
+
|
|
35
|
+
#[cfg(feature = "std")]
|
|
36
|
+
impl Sleeper for StdSleeper {
|
|
37
|
+
fn sleep_ms(&self, ms: u64) {
|
|
38
|
+
std::thread::sleep(std::time::Duration::from_millis(ms));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Function pointer sleeper for custom sleep implementations
|
|
43
|
+
///
|
|
44
|
+
/// Wraps a function pointer that takes milliseconds and performs sleep.
|
|
45
|
+
/// Useful for async runtimes or testing.
|
|
46
|
+
///
|
|
47
|
+
/// # Example
|
|
48
|
+
///
|
|
49
|
+
/// ```rust
|
|
50
|
+
/// use chrono_machines::sleep::{FnSleeper, Sleeper};
|
|
51
|
+
///
|
|
52
|
+
/// // Custom sleep function
|
|
53
|
+
/// fn my_sleep(ms: u64) {
|
|
54
|
+
/// // Custom implementation
|
|
55
|
+
/// std::thread::sleep(std::time::Duration::from_millis(ms));
|
|
56
|
+
/// }
|
|
57
|
+
///
|
|
58
|
+
/// let sleeper = FnSleeper(my_sleep);
|
|
59
|
+
/// sleeper.sleep_ms(100);
|
|
60
|
+
/// ```
|
|
61
|
+
#[derive(Clone, Copy)]
|
|
62
|
+
pub struct FnSleeper(pub fn(u64));
|
|
63
|
+
|
|
64
|
+
impl Sleeper for FnSleeper {
|
|
65
|
+
fn sleep_ms(&self, ms: u64) {
|
|
66
|
+
(self.0)(ms);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[cfg(test)]
|
|
71
|
+
mod tests {
|
|
72
|
+
use super::*;
|
|
73
|
+
|
|
74
|
+
#[cfg(feature = "std")]
|
|
75
|
+
#[test]
|
|
76
|
+
fn test_std_sleeper() {
|
|
77
|
+
let sleeper = StdSleeper;
|
|
78
|
+
let start = std::time::Instant::now();
|
|
79
|
+
sleeper.sleep_ms(10);
|
|
80
|
+
let elapsed = start.elapsed();
|
|
81
|
+
|
|
82
|
+
// Allow some margin for timing precision
|
|
83
|
+
assert!(elapsed.as_millis() >= 9 && elapsed.as_millis() <= 20);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn test_fn_sleeper() {
|
|
88
|
+
fn test_sleep(ms: u64) {
|
|
89
|
+
// In a real test, we'd need interior mutability
|
|
90
|
+
// For this test, we just verify it compiles and runs
|
|
91
|
+
assert!(ms > 0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let sleeper = FnSleeper(test_sleep);
|
|
95
|
+
sleeper.sleep_ms(100);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "chrono_machines_native"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
authors = ["Abdelkader Boudih <terminale@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
description = "Ruby FFI binding for chrono_machines"
|
|
8
|
+
repository = "https://github.com/seuros/chrono_machines"
|
|
9
|
+
publish = false
|
|
10
|
+
|
|
11
|
+
[lib]
|
|
12
|
+
name = "chrono_machines_native"
|
|
13
|
+
crate-type = ["cdylib"]
|
|
14
|
+
|
|
15
|
+
[dependencies]
|
|
16
|
+
chrono-machines = { workspace = true, features = ["std"] }
|
|
17
|
+
magnus = "0.8.2"
|
|
18
|
+
rand = { workspace = true }
|
|
19
|
+
rb-sys = { version = "0.9.117", default-features = false, features = ["stable-api-compiled-fallback"] }
|
|
20
|
+
|
|
21
|
+
[features]
|
|
22
|
+
default = ["std"]
|
|
23
|
+
std = ["chrono-machines/std"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
def create_noop_makefile(message)
|
|
4
|
+
warn message
|
|
5
|
+
warn 'ChronoMachines will fall back to pure Ruby backend.'
|
|
6
|
+
File.write('Makefile', <<~MAKE)
|
|
7
|
+
all:
|
|
8
|
+
@echo '#{message}'
|
|
9
|
+
install:
|
|
10
|
+
@echo '#{message}'
|
|
11
|
+
MAKE
|
|
12
|
+
exit 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Skip native extension compilation outside CRuby
|
|
16
|
+
unless RUBY_ENGINE == 'ruby'
|
|
17
|
+
create_noop_makefile("Skipping native extension on #{RUBY_ENGINE}")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Check if Cargo is available
|
|
21
|
+
def cargo_available?
|
|
22
|
+
system('cargo --version > /dev/null 2>&1')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless cargo_available?
|
|
26
|
+
create_noop_makefile('Skipping native extension (Cargo not found)')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Use rb_sys to compile the Rust extension
|
|
30
|
+
require 'mkmf'
|
|
31
|
+
|
|
32
|
+
# Wrap entire compilation process in error handling to ensure gem install never fails
|
|
33
|
+
begin
|
|
34
|
+
require 'rb_sys/mkmf'
|
|
35
|
+
|
|
36
|
+
require 'pathname'
|
|
37
|
+
|
|
38
|
+
create_rust_makefile('chrono_machines_native/chrono_machines_native') do |r|
|
|
39
|
+
ffi_dir = Pathname(__dir__)
|
|
40
|
+
r.ext_dir = begin
|
|
41
|
+
ffi_dir.relative_path_from(Pathname(Dir.pwd)).to_s
|
|
42
|
+
rescue ArgumentError
|
|
43
|
+
ffi_dir.expand_path.to_s
|
|
44
|
+
end
|
|
45
|
+
# Profile configuration
|
|
46
|
+
r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :release).to_sym
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
makefile_path = File.join(Dir.pwd, 'Makefile')
|
|
50
|
+
if File.exist?(makefile_path)
|
|
51
|
+
manifest_path = File.expand_path(__dir__)
|
|
52
|
+
contents = File.read(makefile_path)
|
|
53
|
+
contents.gsub!(/^RB_SYS_CARGO_MANIFEST_DIR \?=.*$/, "RB_SYS_CARGO_MANIFEST_DIR ?= #{manifest_path}")
|
|
54
|
+
File.write(makefile_path, contents)
|
|
55
|
+
end
|
|
56
|
+
rescue LoadError => e
|
|
57
|
+
# rb_sys not available
|
|
58
|
+
create_noop_makefile("Skipping native extension (rb_sys gem not available: #{e.message})")
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
# Any other compilation setup failure (Rust compilation errors, Makefile generation, etc.)
|
|
61
|
+
create_noop_makefile("Skipping native extension (compilation setup failed: #{e.message})")
|
|
62
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//! Ruby FFI binding for chrono_machines
|
|
2
|
+
//!
|
|
3
|
+
//! This crate provides a Magnus-based Ruby binding for the chrono_machines library.
|
|
4
|
+
//! It exposes a simple helper function for calculating delays with exponential backoff.
|
|
5
|
+
|
|
6
|
+
#![warn(rust_2024_compatibility)]
|
|
7
|
+
#![warn(clippy::all)]
|
|
8
|
+
|
|
9
|
+
use chrono_machines::fibonacci;
|
|
10
|
+
use magnus::{function, Error, Ruby};
|
|
11
|
+
use rand::rngs::StdRng;
|
|
12
|
+
use rand::RngExt;
|
|
13
|
+
use std::cell::RefCell;
|
|
14
|
+
|
|
15
|
+
// Thread-local RNG for performance (avoids reseeding from entropy on every call)
|
|
16
|
+
thread_local! {
|
|
17
|
+
static RNG: RefCell<StdRng> = RefCell::new(rand::make_rng());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Calculate delay using exponential backoff with configurable jitter
|
|
21
|
+
///
|
|
22
|
+
/// # Arguments
|
|
23
|
+
/// * `attempt` - The current attempt number (1-indexed)
|
|
24
|
+
/// * `base_delay` - Base delay in seconds
|
|
25
|
+
/// * `multiplier` - Exponential multiplier
|
|
26
|
+
/// * `max_delay` - Maximum delay cap in seconds
|
|
27
|
+
/// * `jitter_factor` - Jitter multiplier (0.0 = no jitter, 1.0 = full jitter)
|
|
28
|
+
///
|
|
29
|
+
/// # Returns
|
|
30
|
+
/// Calculated delay in seconds with jitter applied
|
|
31
|
+
fn calculate_delay_exponential(
|
|
32
|
+
attempt: i64,
|
|
33
|
+
base_delay: f64,
|
|
34
|
+
multiplier: f64,
|
|
35
|
+
max_delay: f64,
|
|
36
|
+
jitter_factor: f64,
|
|
37
|
+
) -> f64 {
|
|
38
|
+
let jitter_factor = normalize_jitter(jitter_factor);
|
|
39
|
+
let attempt_u8 = attempt.clamp(1, 255) as u8;
|
|
40
|
+
let exponent = attempt_u8.saturating_sub(1) as i32;
|
|
41
|
+
|
|
42
|
+
let base_exponential = base_delay * multiplier.powi(exponent);
|
|
43
|
+
let capped = base_exponential.min(max_delay);
|
|
44
|
+
|
|
45
|
+
apply_jitter(capped, jitter_factor)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Calculate delay using constant backoff with optional jitter
|
|
49
|
+
///
|
|
50
|
+
/// # Arguments
|
|
51
|
+
/// * `_attempt` - The current attempt number (unused for constant backoff)
|
|
52
|
+
/// * `delay` - Constant delay in seconds
|
|
53
|
+
/// * `jitter_factor` - Jitter multiplier (0.0 = no jitter, 1.0 = full jitter)
|
|
54
|
+
///
|
|
55
|
+
/// # Returns
|
|
56
|
+
/// Constant delay with jitter applied
|
|
57
|
+
fn calculate_delay_constant(_attempt: i64, delay: f64, jitter_factor: f64) -> f64 {
|
|
58
|
+
let jitter_factor = normalize_jitter(jitter_factor);
|
|
59
|
+
apply_jitter(delay, jitter_factor)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Calculate delay using Fibonacci backoff with optional jitter
|
|
63
|
+
///
|
|
64
|
+
/// # Arguments
|
|
65
|
+
/// * `attempt` - The current attempt number (1-indexed)
|
|
66
|
+
/// * `base_delay` - Base delay in seconds (multiplied by Fibonacci number)
|
|
67
|
+
/// * `max_delay` - Maximum delay cap in seconds
|
|
68
|
+
/// * `jitter_factor` - Jitter multiplier (0.0 = no jitter, 1.0 = full jitter)
|
|
69
|
+
///
|
|
70
|
+
/// # Returns
|
|
71
|
+
/// Fibonacci-based delay with jitter applied
|
|
72
|
+
fn calculate_delay_fibonacci(
|
|
73
|
+
attempt: i64,
|
|
74
|
+
base_delay: f64,
|
|
75
|
+
max_delay: f64,
|
|
76
|
+
jitter_factor: f64,
|
|
77
|
+
) -> f64 {
|
|
78
|
+
let jitter_factor = normalize_jitter(jitter_factor);
|
|
79
|
+
let attempt_u8 = attempt.clamp(1, 255) as u8;
|
|
80
|
+
|
|
81
|
+
let fib = fibonacci(attempt_u8);
|
|
82
|
+
let base = (base_delay * fib as f64).min(max_delay);
|
|
83
|
+
|
|
84
|
+
apply_jitter(base, jitter_factor)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Normalize jitter factor to [0.0, 1.0] range
|
|
88
|
+
fn normalize_jitter(jitter_factor: f64) -> f64 {
|
|
89
|
+
if jitter_factor.is_nan() {
|
|
90
|
+
1.0
|
|
91
|
+
} else {
|
|
92
|
+
jitter_factor.clamp(0.0, 1.0)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Apply jitter to a base delay value
|
|
97
|
+
fn apply_jitter(base: f64, jitter_factor: f64) -> f64 {
|
|
98
|
+
RNG.with(|rng| {
|
|
99
|
+
let mut rng = rng.borrow_mut();
|
|
100
|
+
let random_scalar: f64 = rng.random_range(0.0..=1.0);
|
|
101
|
+
let jitter_blend = 1.0 - jitter_factor + random_scalar * jitter_factor;
|
|
102
|
+
base * jitter_blend
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Initialize the Ruby extension
|
|
107
|
+
#[magnus::init]
|
|
108
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
109
|
+
// Create ChronoMachinesNative module
|
|
110
|
+
let module = ruby.define_module("ChronoMachinesNative")?;
|
|
111
|
+
|
|
112
|
+
// Expose backoff strategy functions
|
|
113
|
+
module.define_module_function(
|
|
114
|
+
"exponential_delay",
|
|
115
|
+
function!(calculate_delay_exponential, 5),
|
|
116
|
+
)?;
|
|
117
|
+
module.define_module_function("constant_delay", function!(calculate_delay_constant, 3))?;
|
|
118
|
+
module.define_module_function("fibonacci_delay", function!(calculate_delay_fibonacci, 4))?;
|
|
119
|
+
|
|
120
|
+
// Backward compatibility: alias old name to exponential
|
|
121
|
+
module.define_module_function("calculate_delay", function!(calculate_delay_exponential, 5))?;
|
|
122
|
+
|
|
123
|
+
Ok(())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#[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
|
+
}
|