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 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 #[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 #[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 #[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 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
96pub 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 pub const fn new(inner: C, policy: RetryPolicy) -> Self {
114 Self { inner, policy }
115 }
116
117 pub const fn inner(&self) -> &C {
119 &self.inner
120 }
121
122 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 #[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)); }
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)); }
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}