chrono_machines 0.12.1 → 0.12.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8156e7b1ab77d65376f6513e97e068e069aae17600e16167d0693226016107e5
4
- data.tar.gz: d2d1996ed01a736d8e00b0e837ebde668766dbe123a123e5f2a9e52958a8b3b7
3
+ metadata.gz: 3ca1865b7e43357e52ad15d937ce6f55670440f01dafccd23aedf7f630227390
4
+ data.tar.gz: 741a3362ab90bc6c0e92a84adf4d61f96b77e7de0ec06fed6d5c460345e441cb
5
5
  SHA512:
6
- metadata.gz: c77ddf57c2750d96e4b90a59978cb1a33be1a4b72967e36578cd10aed1f7394068a6a84af572a20f3b459c77898f22454c38d321956818057be6084e7e3d6a48
7
- data.tar.gz: 3ef5f8b95eac8491004128c7a2ce64e556c3e2284928c33295adea25b09cd0950af5afc1e88ddee293dee3d4eb9ff7317a88b08951e384d853ac9208b5404ee3
6
+ metadata.gz: 102e6a17a28c093ab80566e9535416ee7b5c944774026125d8e33dc4574185ddf59779f06d1898a3c8d74642adea88a83c4fb29a05713f14b4c9e406d008035c
7
+ data.tar.gz: 45e6645f7072ccca139cfe422b9e32c85c87518e45a61631c24cd5c725a6f88b49c0403cdb8d5c9e4ad41e0d90ea379c823f926b870769f8142bf7eb5ae2fdc5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.12.3](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.12.2...chrono_machines/v0.12.3) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * dedup retry builder failure path and test closures ([846ed9c](https://github.com/seuros/chrono_machines/commit/846ed9cf852a383ea4c5b341162685f243bd7c69))
9
+
10
+ ## [0.12.2](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.12.1...chrono_machines/v0.12.2) (2026-11-06)
11
+
12
+
13
+ ### Features
14
+
15
+ * Backoff tracker for loops that back off on failure and reset on success ([a415dcb](https://github.com/seuros/chrono_machines/commit/a415dcbfc4e11a038049d166fc1cfcdf60917294))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * keep test modules out of the packaged crate ([fa18ff0](https://github.com/seuros/chrono_machines/commit/fa18ff0f9a0980d9fdcb805ae007ecff5cbac553))
21
+
3
22
  ## [0.12.1](https://github.com/seuros/chrono_machines/compare/chrono_machines/v0.12.0...chrono_machines/v0.12.1) (2026-11-01)
4
23
 
5
24
 
data/Cargo.lock CHANGED
@@ -69,7 +69,7 @@ dependencies = [
69
69
 
70
70
  [[package]]
71
71
  name = "chrono-machines"
72
- version = "0.10.0"
72
+ version = "0.11.0"
73
73
  dependencies = [
74
74
  "rand",
75
75
  "tokio",
@@ -77,7 +77,7 @@ dependencies = [
77
77
 
78
78
  [[package]]
79
79
  name = "chrono_machines_native"
80
- version = "0.10.0"
80
+ version = "0.10.1"
81
81
  dependencies = [
82
82
  "chrono-machines",
83
83
  "magnus",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "chrono-machines"
3
- version = "0.10.0"
3
+ version = "0.11.1"
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/", "src/**/tests.rs"]
14
14
 
15
15
  [lib]
16
16
  name = "chrono_machines"
@@ -40,6 +40,7 @@ pub mod policy;
40
40
  #[cfg(any(feature = "std", feature = "alloc"))]
41
41
  pub mod retry;
42
42
  pub mod sleep;
43
+ pub mod streak;
43
44
 
44
45
  /// The `rand` crate this one was built against.
45
46
  ///
@@ -89,6 +90,7 @@ pub use sleep::AsyncSleeper;
89
90
  #[cfg(feature = "std")]
90
91
  pub use sleep::StdSleeper;
91
92
  pub use sleep::{FnSleeper, Sleeper};
93
+ pub use streak::Backoff;
92
94
 
93
95
  #[cfg(feature = "std")]
94
96
  use rand::rngs::StdRng;
@@ -42,22 +42,6 @@ pub enum DelayHint {
42
42
  Halt,
43
43
  }
44
44
 
45
- /// Build a terminal [`RetryError`], firing the `on_failure` callback if present.
46
- fn finalize_failure<E>(
47
- on_failure: Option<&mut FailureCallback<E>>,
48
- kind: RetryErrorKind,
49
- attempt: u8,
50
- max_attempts: u8,
51
- cumulative_delay_ms: u64,
52
- error: E,
53
- ) -> RetryError<E> {
54
- let retry_error = RetryError::new(kind, attempt, max_attempts, cumulative_delay_ms, error);
55
- if let Some(callback) = on_failure {
56
- callback(&retry_error);
57
- }
58
- retry_error
59
- }
60
-
61
45
  /// Reason why a retry operation failed.
62
46
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
63
47
  pub enum RetryErrorKind {
@@ -259,17 +243,7 @@ where
259
243
  F: FnMut() -> Result<T, E>,
260
244
  {
261
245
  fn retry<B: BackoffStrategy>(self, backoff: B) -> RetryBuilder<Self, B, T, E, fn(&E) -> bool> {
262
- RetryBuilder {
263
- operation: self,
264
- backoff,
265
- when: None,
266
- delay_from: None,
267
- notify: None,
268
- on_success: None,
269
- on_failure: None,
270
- _phantom_t: core::marker::PhantomData,
271
- _phantom_e: core::marker::PhantomData,
272
- }
246
+ RetryBuilder::new(self, backoff)
273
247
  }
274
248
  }
275
249
 
@@ -429,6 +403,23 @@ pub struct RetryBuilder<F, B, T, E, W> {
429
403
  _phantom_e: core::marker::PhantomData<E>,
430
404
  }
431
405
 
406
+ impl<F, B, T, E> DefaultRetryBuilder<F, B, T, E> {
407
+ /// A builder with no predicate, hint hook or callbacks.
408
+ const fn new(operation: F, backoff: B) -> Self {
409
+ Self {
410
+ operation,
411
+ backoff,
412
+ when: None,
413
+ delay_from: None,
414
+ notify: None,
415
+ on_success: None,
416
+ on_failure: None,
417
+ _phantom_t: core::marker::PhantomData,
418
+ _phantom_e: core::marker::PhantomData,
419
+ }
420
+ }
421
+ }
422
+
432
423
  /// What the retry loop should do after one attempt.
433
424
  ///
434
425
  /// Produced by [`RetryBuilder::step`], which holds every retry decision —
@@ -581,6 +572,22 @@ where
581
572
  self
582
573
  }
583
574
 
575
+ /// Build the terminal [`RetryError`], firing `on_failure` if present.
576
+ fn fail(
577
+ &mut self,
578
+ kind: RetryErrorKind,
579
+ attempt: u8,
580
+ cumulative_delay_ms: u64,
581
+ error: E,
582
+ ) -> Step<T, E> {
583
+ let max_attempts = self.backoff.max_attempts();
584
+ let retry_error = RetryError::new(kind, attempt, max_attempts, cumulative_delay_ms, error);
585
+ if let Some(callback) = self.on_failure.as_mut() {
586
+ callback(&retry_error);
587
+ }
588
+ Step::Fail(retry_error)
589
+ }
590
+
584
591
  /// Decide what happens after one attempt, firing any callbacks it implies.
585
592
  ///
586
593
  /// This is the whole retry policy. A driver's only jobs are producing the
@@ -592,8 +599,6 @@ where
592
599
  cumulative_delay_ms: u64,
593
600
  rng: &mut R,
594
601
  ) -> Step<T, E> {
595
- let max_attempts = self.backoff.max_attempts();
596
-
597
602
  let error = match result {
598
603
  Ok(value) => {
599
604
  if let Some(ref mut callback) = self.on_success {
@@ -615,26 +620,22 @@ where
615
620
  if let Some(ref predicate) = self.when
616
621
  && !predicate(&error)
617
622
  {
618
- return Step::Fail(finalize_failure(
619
- self.on_failure.as_mut(),
623
+ return self.fail(
620
624
  RetryErrorKind::PredicateRejected,
621
625
  attempt,
622
- max_attempts,
623
626
  cumulative_delay_ms,
624
627
  error,
625
- ));
628
+ );
626
629
  }
627
630
 
628
631
  // Out of attempts? Exhaustion wins over any delay hint.
629
632
  if !self.backoff.should_retry(attempt) {
630
- return Step::Fail(finalize_failure(
631
- self.on_failure.as_mut(),
633
+ return self.fail(
632
634
  RetryErrorKind::Exhausted,
633
635
  attempt,
634
- max_attempts,
635
636
  cumulative_delay_ms,
636
637
  error,
637
- ));
638
+ );
638
639
  }
639
640
 
640
641
  let hint = self
@@ -646,14 +647,12 @@ where
646
647
  DelayHint::Backoff => {
647
648
  // The strategy may still decline to produce a delay.
648
649
  let Some(delay_ms) = self.backoff.delay(attempt, rng) else {
649
- return Step::Fail(finalize_failure(
650
- self.on_failure.as_mut(),
650
+ return self.fail(
651
651
  RetryErrorKind::Exhausted,
652
652
  attempt,
653
- max_attempts,
654
653
  cumulative_delay_ms,
655
654
  error,
656
- ));
655
+ );
657
656
  };
658
657
  delay_ms
659
658
  }
@@ -663,26 +662,22 @@ where
663
662
  .max_delay_ms()
664
663
  .is_some_and(|cap| delay_ms > cap)
665
664
  {
666
- return Step::Fail(finalize_failure(
667
- self.on_failure.as_mut(),
665
+ return self.fail(
668
666
  RetryErrorKind::HintHalted,
669
667
  attempt,
670
- max_attempts,
671
668
  cumulative_delay_ms,
672
669
  error,
673
- ));
670
+ );
674
671
  }
675
672
  delay_ms
676
673
  }
677
674
  DelayHint::Halt => {
678
- return Step::Fail(finalize_failure(
679
- self.on_failure.as_mut(),
675
+ return self.fail(
680
676
  RetryErrorKind::HintHalted,
681
677
  attempt,
682
- max_attempts,
683
678
  cumulative_delay_ms,
684
679
  error,
685
- ));
680
+ );
686
681
  }
687
682
  };
688
683
 
@@ -869,17 +864,7 @@ where
869
864
  self,
870
865
  backoff: B,
871
866
  ) -> RetryBuilder<Self, B, T, E, fn(&E) -> bool> {
872
- RetryBuilder {
873
- operation: self,
874
- backoff,
875
- when: None,
876
- delay_from: None,
877
- notify: None,
878
- on_success: None,
879
- on_failure: None,
880
- _phantom_t: core::marker::PhantomData,
881
- _phantom_e: core::marker::PhantomData,
882
- }
867
+ RetryBuilder::new(self, backoff)
883
868
  }
884
869
  }
885
870
 
@@ -0,0 +1,89 @@
1
+ //! A failure streak and the hold-off it earns, for loops that never give up.
2
+
3
+ use rand::Rng;
4
+
5
+ use crate::BackoffStrategy;
6
+
7
+ /// Failures past this count do not change the delay, so a strategy with
8
+ /// `u8::MAX` attempts never reports itself exhausted.
9
+ const UNBOUNDED: u8 = u8::MAX - 1;
10
+
11
+ /// Consecutive failures of a repeating operation, and the delay a
12
+ /// [`BackoffStrategy`] assigns before the next try.
13
+ ///
14
+ /// [`RetryBuilder`](crate::RetryBuilder) runs one operation until it succeeds
15
+ /// or its attempts run out. A poll loop, a supervised worker or a reconnect is
16
+ /// the other shape: it runs for as long as the process does, holds off longer
17
+ /// while cycles keep failing, and returns to its own cadence on the first
18
+ /// success. This is that bookkeeping, with nothing attached: the caller owns
19
+ /// the loop, the sleep and the stop signal.
20
+ ///
21
+ /// ```
22
+ /// use chrono_machines::rand::{SeedableRng, rngs::StdRng};
23
+ /// use chrono_machines::{Backoff, ExponentialBackoff};
24
+ ///
25
+ /// let mut rng = StdRng::seed_from_u64(7);
26
+ /// let mut backoff = Backoff::new(
27
+ /// ExponentialBackoff::new()
28
+ /// .max_attempts(u8::MAX)
29
+ /// .base_delay_ms(1_000)
30
+ /// .max_delay_ms(30_000)
31
+ /// .jitter_factor(0.0),
32
+ /// );
33
+ ///
34
+ /// assert_eq!(backoff.failed_with_rng(&mut rng), Some(1_000));
35
+ /// assert_eq!(backoff.failed_with_rng(&mut rng), Some(2_000));
36
+ /// backoff.succeeded();
37
+ /// assert_eq!(backoff.failed_with_rng(&mut rng), Some(1_000));
38
+ /// ```
39
+ #[derive(Debug, Clone, Copy)]
40
+ pub struct Backoff<B> {
41
+ strategy: B,
42
+ failures: u8,
43
+ }
44
+
45
+ impl<B: BackoffStrategy> Backoff<B> {
46
+ /// Track failures under `strategy`, starting from none.
47
+ #[must_use]
48
+ pub const fn new(strategy: B) -> Self {
49
+ Self {
50
+ strategy,
51
+ failures: 0,
52
+ }
53
+ }
54
+
55
+ /// Consecutive failures since the last success.
56
+ #[must_use]
57
+ pub const fn failures(&self) -> u8 {
58
+ self.failures
59
+ }
60
+
61
+ /// The strategy the delays come from.
62
+ #[must_use]
63
+ pub const fn strategy(&self) -> &B {
64
+ &self.strategy
65
+ }
66
+
67
+ /// A cycle completed: the streak ends and the next failure starts a new one.
68
+ pub const fn succeeded(&mut self) {
69
+ self.failures = 0;
70
+ }
71
+
72
+ /// A cycle failed: milliseconds to hold off before the next one, or `None`
73
+ /// once the strategy's attempts are spent. A strategy allowing `u8::MAX`
74
+ /// attempts is never spent.
75
+ pub fn failed_with_rng<R: Rng>(&mut self, rng: &mut R) -> Option<u64> {
76
+ self.failures = self.failures.saturating_add(1).min(UNBOUNDED);
77
+ self.strategy.delay(self.failures, rng)
78
+ }
79
+
80
+ /// [`failed_with_rng`](Self::failed_with_rng) with the standard RNG.
81
+ #[cfg(feature = "std")]
82
+ pub fn failed(&mut self) -> Option<u64> {
83
+ let mut rng: rand::rngs::StdRng = rand::make_rng();
84
+ self.failed_with_rng(&mut rng)
85
+ }
86
+ }
87
+
88
+ #[cfg(test)]
89
+ mod tests;
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChronoMachines
4
- VERSION = '0.12.1'
4
+ VERSION = '0.12.3'
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.12.1
4
+ version: 0.12.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -75,6 +75,7 @@ files:
75
75
  - ext/chrono_machines_native/core/src/policy.rs
76
76
  - ext/chrono_machines_native/core/src/retry.rs
77
77
  - ext/chrono_machines_native/core/src/sleep.rs
78
+ - ext/chrono_machines_native/core/src/streak.rs
78
79
  - ext/chrono_machines_native/core/src/test_support.rs
79
80
  - ext/chrono_machines_native/extconf.rb
80
81
  - ext/chrono_machines_native/ffi/Cargo.toml