lastfm_client/client/
retry.rs1use crate::client::HttpClient;
2use crate::error::Result;
3use async_trait::async_trait;
4use std::time::Duration;
5
6#[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 #[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 #[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 #[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 #[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 #[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
96pub struct RetryClient<C> {
98 inner: C,
99 policy: RetryPolicy,
100}
101
102impl<C> RetryClient<C> {
103 pub fn new(inner: C, policy: RetryPolicy) -> Self {
105 Self { inner, policy }
106 }
107
108 pub fn inner(&self) -> &C {
110 &self.inner
111 }
112
113 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 let delay = if let Some(retry_after) = e.retry_after() {
132 retry_after
133 } else {
134 self.policy.backoff(attempts)
135 };
136
137 #[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)); }
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)); }
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}