Skip to main content

talea_client/
retry.rs

1//! Bounded retry with exponential backoff. All client operations are safe
2//! to retry by construction: posts carry idempotency keys, registry writes
3//! are idempotent on id, reads are reads.
4
5use std::time::Duration;
6
7#[derive(Debug, Clone)]
8pub struct RetryPolicy {
9    /// Total attempts including the first (1 = no retries).
10    pub max_attempts: u32,
11    pub base_delay: Duration,
12    pub max_delay: Duration,
13}
14
15impl Default for RetryPolicy {
16    fn default() -> Self {
17        Self {
18            max_attempts: 3,
19            base_delay: Duration::from_millis(200),
20            max_delay: Duration::from_secs(5),
21        }
22    }
23}
24
25impl RetryPolicy {
26    /// No retries: every failure surfaces immediately.
27    pub fn none() -> Self {
28        Self {
29            max_attempts: 1,
30            ..Self::default()
31        }
32    }
33
34    /// Backoff before retry number `attempt` (0-based). A server-provided
35    /// Retry-After overrides the exponential value; both are capped.
36    pub fn delay_for(&self, attempt: u32, retry_after: Option<Duration>) -> Duration {
37        if let Some(ra) = retry_after {
38            return ra.min(self.max_delay);
39        }
40        self.base_delay
41            .saturating_mul(2u32.saturating_pow(attempt))
42            .min(self.max_delay)
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn backoff_doubles_and_caps() {
52        let p = RetryPolicy::default();
53        assert_eq!(p.delay_for(0, None), Duration::from_millis(200));
54        assert_eq!(p.delay_for(1, None), Duration::from_millis(400));
55        assert_eq!(p.delay_for(2, None), Duration::from_millis(800));
56        assert_eq!(p.delay_for(30, None), Duration::from_secs(5)); // capped
57    }
58
59    #[test]
60    fn retry_after_overrides_but_caps() {
61        let p = RetryPolicy::default();
62        assert_eq!(
63            p.delay_for(0, Some(Duration::from_secs(1))),
64            Duration::from_secs(1)
65        );
66        assert_eq!(
67            p.delay_for(0, Some(Duration::from_secs(60))),
68            Duration::from_secs(5)
69        );
70    }
71
72    #[test]
73    fn none_means_single_attempt() {
74        assert_eq!(RetryPolicy::none().max_attempts, 1);
75    }
76}