providers_kit/config/retry.rs
1use std::time::Duration;
2
3use crate::KitError;
4use crate::error::ValidationViolation;
5
6/// Exponential-backoff settings for replayable provider HTTP operations.
7///
8/// The policy applies only to request shapes that the Kit marks safe to replay,
9/// and every retry still runs inside the same shared logical request deadline.
10///
11/// # Examples
12/// ```rust
13/// let policy = providers_kit::config::RetryPolicy::default();
14/// let _ = policy;
15/// ```
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct RetryPolicy {
18 max_retries: u32,
19 initial_backoff: Duration,
20 max_backoff: Duration,
21}
22
23impl Default for RetryPolicy {
24 fn default() -> Self {
25 Self {
26 max_retries: 3,
27 initial_backoff: Duration::from_millis(250),
28 max_backoff: Duration::from_secs(4),
29 }
30 }
31}
32
33impl RetryPolicy {
34 /// Creates validated replay-safe retry policy.
35 ///
36 /// # Errors
37 ///
38 /// Returns a configuration error when retries are enabled with zero initial backoff, or the maximum backoff is shorter than the initial backoff.
39 ///
40 /// # Examples
41 ///
42 /// ```
43 /// use providers_kit::config::RetryPolicy;
44 /// use std::time::Duration;
45 /// let disabled = RetryPolicy::new(0, Duration::ZERO, Duration::ZERO)?;
46 /// assert_ne!(disabled, RetryPolicy::default());
47 /// assert!(RetryPolicy::new(1, Duration::ZERO, Duration::from_secs(1)).is_err());
48 ///
49 /// # Ok::<(), providers_kit::KitError>(())
50 /// ```
51 ///
52 pub fn new(
53 max_retries: u32,
54 initial_backoff: Duration,
55 max_backoff: Duration,
56 ) -> Result<Self, KitError> {
57 if max_retries > 0 && initial_backoff.is_zero() {
58 return Err(KitError::config(
59 ValidationViolation::OutOfRange {
60 field: "retry.initial_backoff",
61 rule: "positive when retries are enabled",
62 },
63 "retry initial backoff must be non-zero when retries are enabled",
64 ));
65 }
66 if max_backoff < initial_backoff {
67 return Err(KitError::config(
68 ValidationViolation::InvalidValue {
69 field: "retry.max_backoff",
70 rule: "at least initial_backoff",
71 },
72 "retry maximum backoff must not be shorter than the initial backoff",
73 ));
74 }
75 Ok(Self {
76 max_retries,
77 initial_backoff,
78 max_backoff,
79 })
80 }
81
82 pub(crate) fn delay_for_attempt(&self, attempt: u32) -> Option<Duration> {
83 if attempt >= self.max_retries {
84 return None;
85 }
86 let multiplier = 2_u32.saturating_pow(attempt);
87 Some(
88 self.initial_backoff
89 .saturating_mul(multiplier)
90 .min(self.max_backoff),
91 )
92 }
93}