chrono_machines 0.5.2 → 0.8.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.
@@ -1,15 +1,24 @@
1
1
  //! Sleep abstraction for no_std compatibility
2
2
  //!
3
- //! This module provides traits and implementations for sleeping/delaying
4
- //! in various environments (std, async, embedded).
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
- /// - Tokio/async-std async sleep
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 async runtimes or testing.
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
- #[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
- }
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
- let sleeper = FnSleeper(test_sleep);
95
- sleeper.sleep_ms(100);
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.0"
3
+ version = "0.8.0"
4
4
  edition = "2024"
5
5
  authors = ["Abdelkader Boudih <terminale@gmail.com>"]
6
6
  license = "MIT"
@@ -7,9 +7,9 @@
7
7
  #![warn(clippy::all)]
8
8
 
9
9
  use chrono_machines::fibonacci;
10
- use magnus::{function, Error, Ruby};
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)
@@ -124,69 +124,4 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
124
124
  }
125
125
 
126
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
- }
127
+ 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 = calculate_delay(attempts)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChronoMachines
4
- VERSION = '0.5.2'
4
+ VERSION = '0.8.0'
5
5
  end
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.5.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih