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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/README.md +1 -1
  4. data/ext/chrono_machines_native/core/Cargo.toml +28 -0
  5. data/ext/chrono_machines_native/core/src/backoff.rs +516 -0
  6. data/ext/chrono_machines_native/core/src/dsl.rs +108 -0
  7. data/ext/chrono_machines_native/core/src/lib.rs +355 -0
  8. data/ext/chrono_machines_native/core/src/policy.rs +189 -0
  9. data/ext/chrono_machines_native/core/src/retry.rs +1511 -0
  10. data/ext/chrono_machines_native/core/src/sleep.rs +97 -0
  11. data/ext/chrono_machines_native/extconf.rb +3 -0
  12. data/ext/chrono_machines_native/ffi/Cargo.toml +23 -0
  13. data/ext/chrono_machines_native/ffi/extconf.rb +62 -0
  14. data/ext/chrono_machines_native/ffi/src/lib.rs +192 -0
  15. data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/common.rs +355 -0
  16. data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/dynamic.rs +276 -0
  17. data/ext/chrono_machines_native/target/debug/build/clang-sys-414c893ed9493fe4/out/macros.rs +49 -0
  18. data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/common.rs +355 -0
  19. data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/dynamic.rs +276 -0
  20. data/ext/chrono_machines_native/target/debug/build/clang-sys-b1d0ba329a011cdd/out/macros.rs +49 -0
  21. 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
  22. 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
  23. 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
  24. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/Cargo.toml +76 -0
  25. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/backoff.rs +497 -0
  26. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/dsl.rs +108 -0
  27. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/lib.rs +352 -0
  28. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/policy.rs +189 -0
  29. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/retry.rs +1489 -0
  30. data/ext/chrono_machines_native/target/package/chrono-machines-0.3.2/src/sleep.rs +97 -0
  31. data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/Cargo.toml +56 -0
  32. data/ext/chrono_machines_native/target/package/chrono_machines_native-0.2.3/src/lib.rs +192 -0
  33. data/lib/chrono_machines/async_support.rb +2 -1
  34. data/lib/chrono_machines/configuration.rb +19 -5
  35. data/lib/chrono_machines/executor.rb +69 -9
  36. data/lib/chrono_machines/native_speedup.rb +61 -0
  37. data/lib/chrono_machines/version.rb +1 -1
  38. data/lib/chrono_machines.rb +1 -1
  39. 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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ffi/extconf'
@@ -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
+ }