Skip to main content

hasp_core/
retry.rs

1use crate::{Backend, Error, SecretString};
2use std::sync::Arc;
3use std::time::Duration;
4use url::Url;
5
6/// Decorator backend that retries transient failures with exponential backoff.
7pub struct RetryBackend {
8    inner: Arc<dyn Backend>,
9    max_retries: u32,
10    base_delay: Duration,
11}
12
13impl RetryBackend {
14    /// Create a new retry decorator.
15    pub fn new(inner: Arc<dyn Backend>) -> Self {
16        Self {
17            inner,
18            max_retries: 3,
19            base_delay: Duration::from_millis(100),
20        }
21    }
22
23    /// Set the maximum number of retry attempts.
24    pub fn max_retries(mut self, n: u32) -> Self {
25        self.max_retries = n;
26        self
27    }
28
29    /// Set the base delay between retries (doubles each attempt).
30    pub fn base_delay(mut self, d: Duration) -> Self {
31        self.base_delay = d;
32        self
33    }
34
35    fn retry(
36        &self,
37        mut op: impl FnMut() -> Result<SecretString, Error>,
38    ) -> Result<SecretString, Error> {
39        let mut last_err = None;
40        for attempt in 0..=self.max_retries {
41            match op() {
42                Ok(val) => return Ok(val),
43                Err(e) => {
44                    if !e.is_transient() || attempt == self.max_retries {
45                        return Err(e);
46                    }
47                    let delay = Self::backoff(attempt);
48                    std::thread::sleep(delay);
49                    last_err = Some(e);
50                }
51            }
52        }
53        Err(last_err.unwrap())
54    }
55
56    fn retry_void(&self, mut op: impl FnMut() -> Result<(), Error>) -> Result<(), Error> {
57        let mut last_err = None;
58        for attempt in 0..=self.max_retries {
59            match op() {
60                Ok(()) => return Ok(()),
61                Err(e) => {
62                    if !e.is_transient() || attempt == self.max_retries {
63                        return Err(e);
64                    }
65                    let delay = Self::backoff(attempt);
66                    std::thread::sleep(delay);
67                    last_err = Some(e);
68                }
69            }
70        }
71        Err(last_err.unwrap())
72    }
73
74    fn retry_bool(&self, mut op: impl FnMut() -> Result<bool, Error>) -> Result<bool, Error> {
75        let mut last_err = None;
76        for attempt in 0..=self.max_retries {
77            match op() {
78                Ok(val) => return Ok(val),
79                Err(e) => {
80                    if !e.is_transient() || attempt == self.max_retries {
81                        return Err(e);
82                    }
83                    let delay = Self::backoff(attempt);
84                    std::thread::sleep(delay);
85                    last_err = Some(e);
86                }
87            }
88        }
89        Err(last_err.unwrap())
90    }
91
92    fn retry_vec(
93        &self,
94        mut op: impl FnMut() -> Result<Vec<crate::Entry>, Error>,
95    ) -> Result<Vec<crate::Entry>, Error> {
96        let mut last_err = None;
97        for attempt in 0..=self.max_retries {
98            match op() {
99                Ok(val) => return Ok(val),
100                Err(e) => {
101                    if !e.is_transient() || attempt == self.max_retries {
102                        return Err(e);
103                    }
104                    let delay = Self::backoff(attempt);
105                    std::thread::sleep(delay);
106                    last_err = Some(e);
107                }
108            }
109        }
110        Err(last_err.unwrap())
111    }
112
113    /// Exponential backoff with deterministic jitter.
114    ///
115    /// delay = base_delay * 2^attempt + jitter
116    /// where jitter = (attempt * 7) % 50 to avoid adding rand.
117    fn backoff(attempt: u32) -> Duration {
118        let base = Duration::from_millis(100);
119        let multiplier = 2_u128.pow(attempt);
120        let exponential = base.as_millis().saturating_mul(multiplier);
121        let jitter = (attempt.wrapping_mul(7) % 50) as u128;
122        Duration::from_millis(u64::try_from(exponential.saturating_add(jitter)).unwrap_or(u64::MAX))
123    }
124}
125
126impl Backend for RetryBackend {
127    fn scheme(&self) -> &'static str {
128        self.inner.scheme()
129    }
130
131    fn get(&self, url: &Url) -> Result<SecretString, Error> {
132        self.retry(|| self.inner.get(url))
133    }
134
135    fn put(&self, url: &Url, value: &SecretString) -> Result<(), Error> {
136        self.retry_void(|| self.inner.put(url, value))
137    }
138
139    fn list(&self, url: &Url) -> Result<Vec<crate::Entry>, Error> {
140        self.retry_vec(|| self.inner.list(url))
141    }
142
143    fn delete(&self, url: &Url) -> Result<(), Error> {
144        self.retry_void(|| self.inner.delete(url))
145    }
146
147    fn exists(&self, url: &Url) -> Result<bool, Error> {
148        self.retry_bool(|| self.inner.exists(url))
149    }
150}