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 +4 -4
- data/CHANGELOG.md +19 -0
- data/Cargo.lock +2 -2
- data/ext/chrono_machines_native/core/Cargo.toml +2 -2
- data/ext/chrono_machines_native/core/src/lib.rs +2 -0
- data/ext/chrono_machines_native/core/src/retry.rs +45 -60
- data/ext/chrono_machines_native/core/src/streak.rs +89 -0
- data/lib/chrono_machines/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ca1865b7e43357e52ad15d937ce6f55670440f01dafccd23aedf7f630227390
|
|
4
|
+
data.tar.gz: 741a3362ab90bc6c0e92a84adf4d61f96b77e7de0ec06fed6d5c460345e441cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
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.
|
|
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
|