execution_policy/retry/
mod.rs1pub mod backoff;
4pub mod budget;
5pub mod jitter;
6
7pub use backoff::Backoff;
8pub use budget::RetryBudget;
9pub use jitter::Jitter;
10
11use std::time::Duration;
12
13use crate::classify::{Classifier, RetryDecision};
14use crate::core::Core;
15
16pub struct Retry<T, E> {
18 max_attempts: u32,
19 max_elapsed: Option<Duration>,
20 backoff: Backoff,
21 jitter: Jitter,
22 classifier: Classifier<T, E>,
23 budget: Option<RetryBudget>,
24}
25
26impl<T, E> std::fmt::Debug for Retry<T, E> {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.debug_struct("Retry")
29 .field("max_attempts", &self.max_attempts)
30 .field("max_elapsed", &self.max_elapsed)
31 .field("backoff", &self.backoff)
32 .field("jitter", &self.jitter)
33 .field("classifier", &self.classifier)
34 .field("budget", &self.budget)
35 .finish()
36 }
37}
38
39impl<T, E> Retry<T, E> {
40 fn base(max_attempts: u32, backoff: Backoff) -> Self {
41 Self {
42 max_attempts,
43 max_elapsed: None,
44 backoff,
45 jitter: Jitter::None,
46 classifier: Classifier::RetryAll,
47 budget: None,
48 }
49 }
50
51 pub fn none() -> Self {
53 Self::base(1, Backoff::fixed(Duration::ZERO))
54 }
55 pub fn fixed(delay: Duration) -> Self {
57 Self::base(3, Backoff::fixed(delay))
58 }
59 pub fn exponential() -> Self {
61 Self::base(
62 3,
63 Backoff::exponential(Duration::from_millis(100), Duration::from_secs(10)),
64 )
65 }
66 pub fn standard() -> Self {
69 Self::base(
70 4,
71 Backoff::exponential(Duration::from_millis(100), Duration::from_secs(2)),
72 )
73 .jitter(Jitter::Full)
74 }
75
76 pub fn max_attempts(mut self, n: u32) -> Self {
77 self.max_attempts = n;
78 self
79 }
80 pub fn max_elapsed(mut self, d: Duration) -> Self {
81 self.max_elapsed = Some(d);
82 self
83 }
84 pub fn base_delay(mut self, d: Duration) -> Self {
85 self.backoff = match self.backoff {
86 Backoff::Exponential { max, .. } => Backoff::Exponential { base: d, max },
87 Backoff::Fixed(_) => Backoff::Fixed(d),
88 };
89 self
90 }
91 pub fn max_delay(mut self, d: Duration) -> Self {
92 if let Backoff::Exponential { base, .. } = self.backoff {
93 self.backoff = Backoff::Exponential { base, max: d };
94 }
95 self
96 }
97 pub fn jitter(mut self, j: Jitter) -> Self {
98 self.jitter = j;
99 self
100 }
101 pub fn budget(mut self, budget: RetryBudget) -> Self {
103 self.budget = Some(budget);
104 self
105 }
106 pub fn when(mut self, pred: impl Fn(&E) -> bool + Send + Sync + 'static) -> Self {
107 self.classifier = Classifier::WhenErr(Box::new(pred));
108 self
109 }
110 pub fn when_outcome(
111 mut self,
112 f: impl Fn(&Result<T, E>) -> RetryDecision + Send + Sync + 'static,
113 ) -> Self {
114 self.classifier = Classifier::WhenOutcome(Box::new(f));
115 self
116 }
117
118 pub(crate) fn max_attempts_value(&self) -> u32 {
119 self.max_attempts
120 }
121 pub(crate) fn max_elapsed_value(&self) -> Option<Duration> {
122 self.max_elapsed
123 }
124 pub(crate) fn decide(&self, outcome: &Result<T, E>) -> RetryDecision {
125 self.classifier.decide(outcome)
126 }
127 pub(crate) fn delay(&self, attempt: u32, core: &dyn Core) -> Duration {
128 let raw = self.backoff.raw_delay(attempt);
129 self.jitter.apply(raw, core.next_u64())
130 }
131 pub(crate) fn budget_ref(&self) -> Option<&RetryBudget> {
132 self.budget.as_ref()
133 }
134}
135
136#[cfg(test)]
137mod retry_tests {
138 use super::*;
139 use crate::core::{ManualClock, TestCore};
140
141 #[test]
142 fn presets_have_expected_attempt_caps() {
143 assert_eq!(Retry::<(), ()>::none().max_attempts_value(), 1);
144 assert_eq!(
145 Retry::<(), ()>::fixed(Duration::ZERO).max_attempts_value(),
146 3
147 );
148 assert_eq!(Retry::<(), ()>::exponential().max_attempts_value(), 3);
149 assert_eq!(Retry::<(), ()>::standard().max_attempts_value(), 4);
150 }
151
152 #[test]
153 fn builder_overrides_schedule() {
154 let r = Retry::<u32, &str>::exponential()
155 .max_attempts(5)
156 .base_delay(Duration::from_millis(20))
157 .max_delay(Duration::from_millis(80));
158 assert_eq!(r.max_attempts_value(), 5);
159 let core = TestCore::new(ManualClock::new());
160 assert_eq!(r.delay(1, &core), Duration::from_millis(20));
161 assert_eq!(r.delay(3, &core), Duration::from_millis(80));
162 assert_eq!(r.delay(9, &core), Duration::from_millis(80));
163 }
164
165 #[test]
166 fn when_predicate_controls_decision() {
167 let r = Retry::<u32, i32>::exponential().when(|e: &i32| *e >= 500);
168 assert_eq!(r.decide(&Err(503)), RetryDecision::Retry);
169 assert_eq!(r.decide(&Err(404)), RetryDecision::Stop);
170 assert_eq!(r.decide(&Ok(1)), RetryDecision::Stop);
171 }
172}