Skip to main content

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}