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 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 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 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 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> RetryClient<C> {
103    /// Create a new retry client wrapping an existing HTTP client
104    pub fn new(inner: C, policy: RetryPolicy) -> Self {
105        Self { inner, policy }
106    }
107
108    /// Get a reference to the inner client
109    pub fn inner(&self) -> &C {
110        &self.inner
111    }
112
113    /// Get a reference to the retry policy
114    pub fn policy(&self) -> &RetryPolicy {
115        &self.policy
116    }
117}
118
119#[async_trait]
120impl<C: HttpClient + Send + Sync> HttpClient for RetryClient<C> {
121    async fn get(&self, url: &str) -> Result<serde_json::Value> {
122        let mut attempts = 0;
123
124        loop {
125            match self.inner.get(url).await {
126                Ok(response) => return Ok(response),
127                Err(e) if e.is_retryable() && attempts < self.policy.max_attempts => {
128                    attempts += 1;
129
130                    // Check if the error specifies a retry delay
131                    let delay = if let Some(retry_after) = e.retry_after() {
132                        retry_after
133                    } else {
134                        self.policy.backoff(attempts)
135                    };
136
137                    // Log the retry attempt (in production, use tracing)
138                    #[cfg(debug_assertions)]
139                    eprintln!(
140                        "Retrying request (attempt {}/{}) after {:?}...",
141                        attempts, self.policy.max_attempts, delay
142                    );
143
144                    tokio::time::sleep(delay).await;
145                }
146                Err(e) => return Err(e),
147            }
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_exponential_backoff() {
158        let policy = RetryPolicy::exponential(3);
159
160        assert_eq!(policy.backoff(0), Duration::from_millis(100));
161        assert_eq!(policy.backoff(1), Duration::from_millis(200));
162        assert_eq!(policy.backoff(2), Duration::from_millis(400));
163        assert_eq!(policy.backoff(3), Duration::from_millis(800));
164        assert_eq!(policy.backoff(10), Duration::from_secs(30)); // Max delay
165    }
166
167    #[test]
168    fn test_linear_backoff() {
169        let policy = RetryPolicy::linear(3);
170
171        assert_eq!(policy.backoff(1), Duration::from_secs(1));
172        assert_eq!(policy.backoff(2), Duration::from_secs(2));
173        assert_eq!(policy.backoff(3), Duration::from_secs(3));
174        assert_eq!(policy.backoff(20), Duration::from_secs(10)); // Max delay
175    }
176
177    #[test]
178    fn test_custom_policy() {
179        let policy =
180            RetryPolicy::custom(5, Duration::from_millis(500), Duration::from_secs(5), true);
181
182        assert_eq!(policy.max_attempts(), 5);
183        assert_eq!(policy.backoff(0), Duration::from_millis(500));
184        assert_eq!(policy.backoff(1), Duration::from_millis(1000));
185    }
186
187    #[tokio::test]
188    async fn test_retry_client_success() {
189        use crate::client::MockClient;
190        use serde_json::json;
191
192        let mock = MockClient::new().with_response("test.method", json!({"success": true}));
193
194        let retry_client = RetryClient::new(mock, RetryPolicy::exponential(3));
195
196        let result = retry_client
197            .get("http://example.com?method=test.method")
198            .await;
199        assert!(result.is_ok());
200    }
201
202    #[test]
203    fn test_default_policy() {
204        let policy = RetryPolicy::default();
205        assert_eq!(policy.max_attempts(), 3);
206    }
207}