1use crate::{LastFmError, Result};
2use std::future::Future;
3
4#[derive(Debug, Clone)]
6pub struct RateLimitConfig {
7 pub detect_by_status: bool,
9 pub detect_by_patterns: bool,
11 pub patterns: Vec<String>,
13 pub custom_patterns: Vec<String>,
15}
16
17impl Default for RateLimitConfig {
18 fn default() -> Self {
19 Self {
20 detect_by_status: true,
21 detect_by_patterns: true,
22 patterns: vec![
23 "you've tried to log in too many times".to_string(),
24 "you're requesting too many pages".to_string(),
25 "slow down".to_string(),
26 "too fast".to_string(),
27 "rate limit".to_string(),
28 "throttled".to_string(),
29 "temporarily blocked".to_string(),
30 "temporarily restricted".to_string(),
31 "captcha".to_string(),
32 "verify you're human".to_string(),
33 "prove you're not a robot".to_string(),
34 "security check".to_string(),
35 "service temporarily unavailable".to_string(),
36 "quota exceeded".to_string(),
37 "limit exceeded".to_string(),
38 "daily limit".to_string(),
39 ],
40 custom_patterns: vec![],
41 }
42 }
43}
44
45impl RateLimitConfig {
46 pub fn disabled() -> Self {
48 Self {
49 detect_by_status: false,
50 detect_by_patterns: false,
51 patterns: vec![],
52 custom_patterns: vec![],
53 }
54 }
55
56 pub fn status_only() -> Self {
58 Self {
59 detect_by_status: true,
60 detect_by_patterns: false,
61 patterns: vec![],
62 custom_patterns: vec![],
63 }
64 }
65
66 pub fn patterns_only() -> Self {
68 Self {
69 detect_by_status: false,
70 detect_by_patterns: true,
71 ..Default::default()
72 }
73 }
74
75 pub fn custom_patterns_only(patterns: Vec<String>) -> Self {
77 Self {
78 detect_by_status: false,
79 detect_by_patterns: false,
80 patterns: vec![],
81 custom_patterns: patterns,
82 }
83 }
84
85 pub fn with_custom_patterns(mut self, patterns: Vec<String>) -> Self {
87 self.custom_patterns = patterns;
88 self
89 }
90
91 pub fn with_patterns(mut self, patterns: Vec<String>) -> Self {
93 self.patterns = patterns;
94 self
95 }
96}
97
98#[derive(Debug, Clone, Default)]
100pub struct ClientConfig {
101 pub retry: RetryConfig,
103 pub rate_limit: RateLimitConfig,
105}
106
107impl ClientConfig {
108 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn with_retries_disabled() -> Self {
115 Self {
116 retry: RetryConfig::disabled(),
117 rate_limit: RateLimitConfig::default(),
118 }
119 }
120
121 pub fn with_rate_limiting_disabled() -> Self {
123 Self {
124 retry: RetryConfig::default(),
125 rate_limit: RateLimitConfig::disabled(),
126 }
127 }
128
129 pub fn minimal() -> Self {
131 Self {
132 retry: RetryConfig::disabled(),
133 rate_limit: RateLimitConfig::disabled(),
134 }
135 }
136
137 pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
139 self.retry = retry_config;
140 self
141 }
142
143 pub fn with_rate_limit_config(mut self, rate_limit_config: RateLimitConfig) -> Self {
145 self.rate_limit = rate_limit_config;
146 self
147 }
148
149 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
151 self.retry.max_retries = max_retries;
152 self.retry.enabled = max_retries > 0;
153 self
154 }
155
156 pub fn with_retry_delays(mut self, base_delay: u64, max_delay: u64) -> Self {
158 self.retry.base_delay = base_delay;
159 self.retry.max_delay = max_delay;
160 self
161 }
162
163 pub fn with_custom_rate_limit_patterns(mut self, patterns: Vec<String>) -> Self {
165 self.rate_limit.custom_patterns = patterns;
166 self
167 }
168
169 pub fn with_status_detection(mut self, enabled: bool) -> Self {
171 self.rate_limit.detect_by_status = enabled;
172 self
173 }
174
175 pub fn with_pattern_detection(mut self, enabled: bool) -> Self {
177 self.rate_limit.detect_by_patterns = enabled;
178 self
179 }
180}
181
182#[derive(Debug, Clone)]
184pub struct RetryConfig {
185 pub max_retries: u32,
187 pub base_delay: u64,
189 pub max_delay: u64,
191 pub enabled: bool,
193}
194
195impl Default for RetryConfig {
196 fn default() -> Self {
197 Self {
198 max_retries: 3,
199 base_delay: 5,
200 max_delay: 300, enabled: true,
202 }
203 }
204}
205
206impl RetryConfig {
207 pub fn disabled() -> Self {
209 Self {
210 max_retries: 0,
211 base_delay: 5,
212 max_delay: 300,
213 enabled: false,
214 }
215 }
216
217 pub fn with_retries(max_retries: u32) -> Self {
219 Self {
220 max_retries,
221 enabled: max_retries > 0,
222 ..Default::default()
223 }
224 }
225
226 pub fn with_delays(base_delay: u64, max_delay: u64) -> Self {
228 Self {
229 base_delay,
230 max_delay,
231 ..Default::default()
232 }
233 }
234}
235
236#[derive(Debug)]
238pub struct RetryResult<T> {
239 pub result: T,
241 pub attempts_made: u32,
243 pub total_retry_time: u64,
245}
246
247pub async fn retry_with_backoff<T, F, Fut, OnRateLimit>(
261 config: RetryConfig,
262 operation_name: &str,
263 mut operation: F,
264 mut on_rate_limit: OnRateLimit,
265) -> Result<RetryResult<T>>
266where
267 F: FnMut() -> Fut,
268 Fut: Future<Output = Result<T>>,
269 OnRateLimit: FnMut(u64, &str),
270{
271 let mut retries = 0;
272 let mut total_retry_time = 0;
273
274 loop {
275 match operation().await {
276 Ok(result) => {
277 return Ok(RetryResult {
278 result,
279 attempts_made: retries,
280 total_retry_time,
281 });
282 }
283 Err(LastFmError::RateLimit { retry_after }) => {
284 if !config.enabled || retries >= config.max_retries {
285 if !config.enabled {
286 log::debug!("Retries disabled for {operation_name} operation");
287 } else {
288 log::warn!(
289 "Max retries ({}) exceeded for {operation_name} operation",
290 config.max_retries
291 );
292 }
293 return Err(LastFmError::RateLimit { retry_after });
294 }
295
296 let base_backoff = config.base_delay * 2_u64.pow(retries);
298 let delay = std::cmp::min(
299 std::cmp::min(retry_after + base_backoff, config.max_delay),
300 retry_after + (retries as u64 * 30), );
302
303 log::info!(
304 "{} rate limited. Waiting {} seconds before retry {} of {}",
305 operation_name,
306 delay,
307 retries + 1,
308 config.max_retries
309 );
310
311 on_rate_limit(delay, operation_name);
313
314 tokio::time::sleep(std::time::Duration::from_secs(delay)).await;
315 retries += 1;
316 total_retry_time += delay;
317 }
318 Err(other_error) => {
319 return Err(other_error);
320 }
321 }
322 }
323}
324
325pub async fn retry_operation<T, F, Fut>(
327 config: RetryConfig,
328 operation_name: &str,
329 operation: F,
330) -> Result<RetryResult<T>>
331where
332 F: FnMut() -> Fut,
333 Fut: Future<Output = Result<T>>,
334{
335 retry_with_backoff(config, operation_name, operation, |delay, op_name| {
336 log::debug!("Rate limited during {op_name}: waiting {delay} seconds");
337 })
338 .await
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use std::sync::atomic::{AtomicU32, Ordering};
345 use std::sync::Arc;
346
347 #[tokio::test]
348 async fn test_successful_operation() {
349 let config = RetryConfig {
350 max_retries: 3,
351 base_delay: 1,
352 max_delay: 60,
353 enabled: true,
354 };
355
356 let result = retry_operation(config, "test", || async { Ok::<i32, LastFmError>(42) }).await;
357
358 assert!(result.is_ok());
359 let retry_result = result.unwrap();
360 assert_eq!(retry_result.result, 42);
361 assert_eq!(retry_result.attempts_made, 0);
362 assert_eq!(retry_result.total_retry_time, 0);
363 }
364
365 #[tokio::test]
366 async fn test_retry_on_rate_limit() {
367 let config = RetryConfig {
368 max_retries: 2,
369 base_delay: 1,
370 max_delay: 60,
371 enabled: true,
372 };
373
374 let call_count = Arc::new(AtomicU32::new(0));
375 let call_count_clone = call_count.clone();
376
377 let result = retry_operation(config, "test", move || {
378 let count = call_count_clone.fetch_add(1, Ordering::SeqCst);
379 async move {
380 if count < 2 {
381 Err(LastFmError::RateLimit { retry_after: 1 })
382 } else {
383 Ok::<i32, LastFmError>(42)
384 }
385 }
386 })
387 .await;
388
389 assert!(result.is_ok());
390 let retry_result = result.unwrap();
391 assert_eq!(retry_result.result, 42);
392 assert_eq!(retry_result.attempts_made, 2);
393 assert!(retry_result.total_retry_time >= 2); }
395
396 #[tokio::test]
397 async fn test_max_retries_exceeded() {
398 let config = RetryConfig {
399 max_retries: 1,
400 base_delay: 1,
401 max_delay: 60,
402 enabled: true,
403 };
404
405 let result = retry_operation(config, "test", || async {
406 Err::<i32, LastFmError>(LastFmError::RateLimit { retry_after: 1 })
407 })
408 .await;
409
410 assert!(result.is_err());
411 match result.unwrap_err() {
412 LastFmError::RateLimit { .. } => {} other => panic!("Expected rate limit error, got: {other:?}"),
414 }
415 }
416
417 #[tokio::test]
418 async fn test_retries_disabled() {
419 let config = RetryConfig::disabled();
420
421 let result = retry_operation(config, "test", || async {
422 Err::<i32, LastFmError>(LastFmError::RateLimit { retry_after: 1 })
423 })
424 .await;
425
426 assert!(result.is_err());
427 match result.unwrap_err() {
428 LastFmError::RateLimit { .. } => {} other => panic!("Expected rate limit error, got: {other:?}"),
430 }
431 }
432}