1pub 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, RetryAfterExtractor, 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 retry_after: Option<RetryAfterExtractor<E>>,
25}
26
27impl<T, E> std::fmt::Debug for Retry<T, E> {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 f.debug_struct("Retry")
30 .field("max_attempts", &self.max_attempts)
31 .field("max_elapsed", &self.max_elapsed)
32 .field("backoff", &self.backoff)
33 .field("jitter", &self.jitter)
34 .field("classifier", &self.classifier)
35 .field("budget", &self.budget)
36 .field("retry_after", &self.retry_after.as_ref().map(|_| "<fn>"))
37 .finish()
38 }
39}
40
41impl<T, E> Retry<T, E> {
42 fn base(max_attempts: u32, backoff: Backoff) -> Self {
43 Self {
44 max_attempts,
45 max_elapsed: None,
46 backoff,
47 jitter: Jitter::None,
48 classifier: Classifier::RetryAll,
49 budget: None,
50 retry_after: None,
51 }
52 }
53
54 pub fn none() -> Self {
56 Self::base(1, Backoff::fixed(Duration::ZERO))
57 }
58 pub fn fixed(delay: Duration) -> Self {
60 Self::base(3, Backoff::fixed(delay))
61 }
62 pub fn exponential() -> Self {
64 Self::base(
65 3,
66 Backoff::exponential(Duration::from_millis(100), Duration::from_secs(10)),
67 )
68 }
69 pub fn standard() -> Self {
72 Self::base(
73 4,
74 Backoff::exponential(Duration::from_millis(100), Duration::from_secs(2)),
75 )
76 .jitter(Jitter::Full)
77 }
78
79 pub fn max_attempts(mut self, n: u32) -> Self {
80 self.max_attempts = n;
81 self
82 }
83 pub fn max_elapsed(mut self, d: Duration) -> Self {
84 self.max_elapsed = Some(d);
85 self
86 }
87 pub fn base_delay(mut self, d: Duration) -> Self {
88 self.backoff = match self.backoff {
89 Backoff::Exponential { max, .. } => Backoff::Exponential { base: d, max },
90 Backoff::Fixed(_) => Backoff::Fixed(d),
91 };
92 self
93 }
94 pub fn max_delay(mut self, d: Duration) -> Self {
95 if let Backoff::Exponential { base, .. } = self.backoff {
96 self.backoff = Backoff::Exponential { base, max: d };
97 }
98 self
99 }
100 pub fn jitter(mut self, j: Jitter) -> Self {
101 self.jitter = j;
102 self
103 }
104 pub fn budget(mut self, budget: RetryBudget) -> Self {
106 self.budget = Some(budget);
107 self
108 }
109 pub fn when(mut self, pred: impl Fn(&E) -> bool + Send + Sync + 'static) -> Self {
110 self.classifier = Classifier::WhenErr(Box::new(pred));
111 self
112 }
113 pub fn when_outcome(
114 mut self,
115 f: impl Fn(&Result<T, E>) -> RetryDecision + Send + Sync + 'static,
116 ) -> Self {
117 self.classifier = Classifier::WhenOutcome(Box::new(f));
118 self
119 }
120
121 pub fn retry_after(
131 mut self,
132 f: impl Fn(&E) -> Option<Duration> + Send + Sync + 'static,
133 ) -> Self {
134 self.retry_after = Some(Box::new(f));
135 self
136 }
137
138 pub(crate) fn max_attempts_value(&self) -> u32 {
139 self.max_attempts
140 }
141 pub(crate) fn max_elapsed_value(&self) -> Option<Duration> {
142 self.max_elapsed
143 }
144 pub(crate) fn decide(&self, outcome: &Result<T, E>) -> RetryDecision {
145 self.classifier.decide(outcome)
146 }
147 pub(crate) fn delay(&self, attempt: u32, core: &dyn Core, last_err: Option<&E>) -> Duration {
155 let backoff_raw = self.backoff.raw_delay(attempt);
156 let hint = last_err
158 .zip(self.retry_after.as_deref())
159 .and_then(|(e, f)| f(e));
160 let raw = match hint {
161 Some(h) if h > backoff_raw => h,
162 _ => backoff_raw,
163 };
164 self.jitter.apply(raw, core.next_u64())
165 }
166 pub(crate) fn budget_ref(&self) -> Option<&RetryBudget> {
167 self.budget.as_ref()
168 }
169 pub(crate) fn retry_after_hint(&self, err: &E) -> Option<Duration> {
171 self.retry_after.as_deref().and_then(|f| f(err))
172 }
173}
174
175#[cfg(all(test, feature = "test-util"))]
176mod retry_tests {
177 use super::*;
178 use crate::core::{ManualClock, TestCore};
179
180 #[test]
181 fn presets_have_expected_attempt_caps() {
182 assert_eq!(Retry::<(), ()>::none().max_attempts_value(), 1);
183 assert_eq!(
184 Retry::<(), ()>::fixed(Duration::ZERO).max_attempts_value(),
185 3
186 );
187 assert_eq!(Retry::<(), ()>::exponential().max_attempts_value(), 3);
188 assert_eq!(Retry::<(), ()>::standard().max_attempts_value(), 4);
189 }
190
191 #[test]
192 fn builder_overrides_schedule() {
193 let r = Retry::<u32, &str>::exponential()
194 .max_attempts(5)
195 .base_delay(Duration::from_millis(20))
196 .max_delay(Duration::from_millis(80));
197 assert_eq!(r.max_attempts_value(), 5);
198 let core = TestCore::new(ManualClock::new());
199 assert_eq!(r.delay(1, &core, None), Duration::from_millis(20));
200 assert_eq!(r.delay(3, &core, None), Duration::from_millis(80));
201 assert_eq!(r.delay(9, &core, None), Duration::from_millis(80));
202 }
203
204 #[test]
205 fn when_predicate_controls_decision() {
206 let r = Retry::<u32, i32>::exponential().when(|e: &i32| *e >= 500);
207 assert_eq!(r.decide(&Err(503)), RetryDecision::Retry);
208 assert_eq!(r.decide(&Err(404)), RetryDecision::Stop);
209 assert_eq!(r.decide(&Ok(1)), RetryDecision::Stop);
210 }
211}