Skip to main content

lastfm_client/client/
retry.rs

1use crate::client::HttpClient;
2use crate::error::Result;
3use async_trait::async_trait;
4use std::time::Duration;
5
6/// Retry policy configuration
7#[derive(Debug, Clone)]
8pub struct RetryPolicy {
9    max_attempts: u32,
10    base_delay: Duration,
11    max_delay: Duration,
12    exponential: bool,
13}
14
15impl RetryPolicy {
16    /// Create an exponential backoff retry policy
17    ///
18    /// Delays increase exponentially: `base_delay` * 2^attempt
19    ///
20    /// # Example
21    /// ```
22    /// use lastfm_client::client::RetryPolicy;
23    ///
24    /// let policy = RetryPolicy::exponential(3); // Max 3 retries
25    /// ```
26    #[must_use]
27    pub const fn exponential(max_attempts: u32) -> Self {
28        Self {
29            max_attempts,
30            base_delay: Duration::from_millis(100),
31            max_delay: Duration::from_secs(30),
32            exponential: true,
33        }
34    }
35
36    /// Create a linear backoff retry policy
37    ///
38    /// Delays increase linearly: `base_delay` * attempt
39    ///
40    /// # Example
41    /// ```
42    /// use lastfm_client::client::RetryPolicy;
43    ///
44    /// let policy = RetryPolicy::linear(3); // Max 3 retries
45    /// ```
46    #[must_use]
47    pub const fn linear(max_attempts: u32) -> Self {
48        Self {
49            max_attempts,
50            base_delay: Duration::from_secs(1),
51            max_delay: Duration::from_secs(10),
52            exponential: false,
53        }
54    }
55
56    /// Create a custom retry policy
57    #[must_use]
58    pub const fn custom(
59        max_attempts: u32,
60        base_delay: Duration,
61        max_delay: Duration,
62        exponential: bool,
63    ) -> Self {
64        Self {
65            max_attempts,
66            base_delay,
67            max_delay,
68            exponential,
69        }
70    }
71
72    /// Calculate backoff delay for a given attempt
73    #[must_use]
74    pub fn backoff(&self, attempt: u32) -> Duration {
75        if self.exponential {
76            let delay = self.base_delay * 2_u32.saturating_pow(attempt);
77            delay.min(self.max_delay)
78        } else {
79            (self.base_delay * attempt).min(self.max_delay)
80        }
81    }
82
83    /// Get maximum number of attempts
84    #[must_use]
85    pub const fn max_attempts(&self) -> u32 {
86        self.max_attempts
87    }
88}
89
90impl Default for RetryPolicy {
91    fn default() -> Self {
92        Self::exponential(3)
93    }
94}
95
96/// HTTP client wrapper that adds retry logic
97pub struct RetryClient<C> {
98    inner: C,
99    policy: RetryPolicy,
100}
101
102impl<C: std::fmt::Debug> std::fmt::Debug for RetryClient<C> {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("RetryClient")
105            .field("inner", &self.inner)
106            .field("policy", &self.policy)
107            .finish()
108    }
109}
110
111impl<C> RetryClient<C> {
112    /// Create a new retry client wrapping an existing HTTP client
113    pub const fn new(inner: C, policy: RetryPolicy) -> Self {
114        Self { inner, policy }
115    }
116
117    /// Get a reference to the inner client
118    pub const fn inner(&self) -> &C {
119        &self.inner
120    }
121
122    /// Get a reference to the retry policy
123    pub const fn policy(&self) -> &RetryPolicy {
124        &self.policy
125    }
126}
127
128#[async_trait]
129impl<C: HttpClient + Send + Sync> HttpClient for RetryClient<C> {
130    async fn get(&self, url: &str) -> Result<serde_json::Value> {
131        let mut attempts = 0;
132
133        loop {
134            match self.inner.get(url).await {
135                Ok(response) => return Ok(response),
136                Err(e) if e.is_retryable() && attempts < self.policy.max_attempts => {
137                    attempts += 1;
138
139                    let delay = e
140                        .retry_after()
141                        .unwrap_or_else(|| self.policy.backoff(attempts));
142
143                    // Log the retry attempt (in production, use tracing)
144                    #[cfg(debug_assertions)]
145                    eprintln!(
146                        "Retrying request (attempt {}/{}) after {:?}...",
147                        attempts, self.policy.max_attempts, delay
148                    );
149
150                    tokio::time::sleep(delay).await;
151                }
152                Err(e) => return Err(e),
153            }
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_exponential_backoff() {
164        let policy = RetryPolicy::exponential(3);
165
166        assert_eq!(policy.backoff(0), Duration::from_millis(100));
167        assert_eq!(policy.backoff(1), Duration::from_millis(200));
168        assert_eq!(policy.backoff(2), Duration::from_millis(400));
169        assert_eq!(policy.backoff(3), Duration::from_millis(800));
170        assert_eq!(policy.backoff(10), Duration::from_secs(30)); // Max delay
171    }
172
173    #[test]
174    fn test_linear_backoff() {
175        let policy = RetryPolicy::linear(3);
176
177        assert_eq!(policy.backoff(1), Duration::from_secs(1));
178        assert_eq!(policy.backoff(2), Duration::from_secs(2));
179        assert_eq!(policy.backoff(3), Duration::from_secs(3));
180        assert_eq!(policy.backoff(20), Duration::from_secs(10)); // Max delay
181    }
182
183    #[test]
184    fn test_custom_policy() {
185        let policy =
186            RetryPolicy::custom(5, Duration::from_millis(500), Duration::from_secs(5), true);
187
188        assert_eq!(policy.max_attempts(), 5);
189        assert_eq!(policy.backoff(0), Duration::from_millis(500));
190        assert_eq!(policy.backoff(1), Duration::from_millis(1000));
191    }
192
193    #[tokio::test]
194    async fn test_retry_client_success() {
195        use crate::client::MockClient;
196        use serde_json::json;
197
198        let mock = MockClient::new().with_response("test.method", json!({"success": true}));
199
200        let retry_client = RetryClient::new(mock, RetryPolicy::exponential(3));
201
202        let result = retry_client
203            .get("http://example.com?method=test.method")
204            .await;
205        assert!(result.is_ok());
206    }
207
208    #[test]
209    fn test_default_policy() {
210        let policy = RetryPolicy::default();
211        assert_eq!(policy.max_attempts(), 3);
212    }
213}