1use crate::error::{ConfigValidationError, Error, ValidationResult};
11use regex::Regex;
12use std::sync::LazyLock;
13use std::time::Duration;
14
15static SERVER_ERROR_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
20 Regex::new(
21 r"(?i)\b(500|502|503|504)\b|internal server error|bad gateway|service unavailable|gateway timeout"
22 ).expect("Invalid server error regex pattern")
23});
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum RetryStrategyType {
28 Fixed,
30 Exponential,
32 Linear,
34}
35
36#[derive(Debug, Clone)]
38pub struct RetryConfig {
39 pub max_retries: u32,
41 pub strategy_type: RetryStrategyType,
43 pub base_delay_ms: u64,
45 pub max_delay_ms: u64,
47 pub retry_on_network_error: bool,
49 pub retry_on_rate_limit: bool,
51 pub retry_on_server_error: bool,
53 pub retry_on_timeout: bool,
55 pub jitter_factor: f64,
57}
58
59impl Default for RetryConfig {
60 fn default() -> Self {
61 Self {
62 max_retries: 3,
63 strategy_type: RetryStrategyType::Exponential,
64 base_delay_ms: 100,
65 max_delay_ms: 30000,
66 retry_on_network_error: true,
67 retry_on_rate_limit: true,
68 retry_on_server_error: true,
69 retry_on_timeout: true,
70 jitter_factor: 0.1,
71 }
72 }
73}
74
75impl RetryConfig {
76 pub fn conservative() -> Self {
78 Self {
79 max_retries: 2,
80 strategy_type: RetryStrategyType::Fixed,
81 base_delay_ms: 500,
82 max_delay_ms: 5000,
83 retry_on_network_error: true,
84 retry_on_rate_limit: true,
85 retry_on_server_error: false,
86 retry_on_timeout: false,
87 jitter_factor: 0.0,
88 }
89 }
90
91 pub fn aggressive() -> Self {
93 Self {
94 max_retries: 5,
95 strategy_type: RetryStrategyType::Exponential,
96 base_delay_ms: 200,
97 max_delay_ms: 60000,
98 retry_on_network_error: true,
99 retry_on_rate_limit: true,
100 retry_on_server_error: true,
101 retry_on_timeout: true,
102 jitter_factor: 0.2,
103 }
104 }
105
106 pub fn rate_limit_only() -> Self {
108 Self {
109 max_retries: 3,
110 strategy_type: RetryStrategyType::Linear,
111 base_delay_ms: 2000,
112 max_delay_ms: 10000,
113 retry_on_network_error: false,
114 retry_on_rate_limit: true,
115 retry_on_server_error: false,
116 retry_on_timeout: false,
117 jitter_factor: 0.0,
118 }
119 }
120
121 pub fn validate(&self) -> Result<ValidationResult, ConfigValidationError> {
152 let warnings = Vec::new();
153
154 if self.max_retries > 10 {
156 return Err(ConfigValidationError::too_high(
157 "max_retries",
158 self.max_retries,
159 10,
160 ));
161 }
162
163 if self.base_delay_ms < 10 {
165 return Err(ConfigValidationError::too_low(
166 "base_delay_ms",
167 self.base_delay_ms,
168 10,
169 ));
170 }
171
172 Ok(ValidationResult::with_warnings(warnings))
173 }
174}
175
176#[derive(Debug, Clone)]
178pub struct RetryStrategy {
179 config: RetryConfig,
180}
181
182impl RetryStrategy {
183 pub fn new(config: RetryConfig) -> Self {
185 Self { config }
186 }
187
188 pub fn default_strategy() -> Self {
190 Self::new(RetryConfig::default())
191 }
192
193 pub fn should_retry(&self, error: &Error, attempt: u32) -> bool {
204 if attempt > self.config.max_retries {
205 return false;
206 }
207 match error {
208 Error::Network(_) => self.config.retry_on_network_error,
209 Error::RateLimit { .. } => self.config.retry_on_rate_limit,
210 Error::Exchange(details) => {
211 if self.config.retry_on_server_error && Self::is_server_error(&details.message) {
212 return true;
213 }
214 if self.config.retry_on_timeout && Self::is_timeout_error(&details.message) {
215 return true;
216 }
217 false
218 }
219 _ => false,
220 }
221 }
222
223 pub fn calculate_delay(&self, attempt: u32, error: &Error) -> Duration {
234 let base_delay = match self.config.strategy_type {
235 RetryStrategyType::Fixed => self.config.base_delay_ms,
236 RetryStrategyType::Exponential => {
237 self.config.base_delay_ms * 2_u64.pow(attempt.saturating_sub(1))
238 }
239 RetryStrategyType::Linear => self.config.base_delay_ms * u64::from(attempt),
240 };
241
242 let mut delay = base_delay.min(self.config.max_delay_ms);
243
244 if matches!(error, Error::RateLimit { .. }) {
245 delay = delay.max(2000);
246 }
247 if self.config.jitter_factor > 0.0 {
248 delay = self.apply_jitter(delay);
249 }
250
251 Duration::from_millis(delay)
252 }
253
254 fn apply_jitter(&self, delay_ms: u64) -> u64 {
256 use rand::Rng;
257 let mut rng = rand::rngs::ThreadRng::default();
258 #[allow(clippy::cast_precision_loss)]
259 #[allow(clippy::cast_possible_truncation)]
260 let jitter_range = (delay_ms as f64 * self.config.jitter_factor) as u64;
261 let jitter = rng.random_range(0..=jitter_range);
262 delay_ms + jitter
263 }
264
265 pub fn is_server_error(msg: &str) -> bool {
292 Self::is_server_error_message(msg)
293 }
294
295 pub fn is_server_error_code(status: u16) -> bool {
325 (500..600).contains(&status)
326 }
327
328 pub fn is_server_error_message(msg: &str) -> bool {
362 SERVER_ERROR_PATTERN.is_match(msg)
363 }
364
365 fn is_timeout_error(msg: &str) -> bool {
367 let msg_lower = msg.to_lowercase();
368 msg_lower.contains("timeout")
369 || msg_lower.contains("timed out")
370 || msg_lower.contains("408")
371 }
372
373 pub fn config(&self) -> &RetryConfig {
375 &self.config
376 }
377
378 pub fn max_retries(&self) -> u32 {
380 self.config.max_retries
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn test_retry_config_default() {
390 let config = RetryConfig::default();
391 assert_eq!(config.max_retries, 3);
392 assert_eq!(config.strategy_type, RetryStrategyType::Exponential);
393 assert_eq!(config.base_delay_ms, 100);
394 assert!(config.retry_on_network_error);
395 assert!(config.retry_on_rate_limit);
396 }
397
398 #[test]
399 fn test_retry_config_conservative() {
400 let config = RetryConfig::conservative();
401 assert_eq!(config.max_retries, 2);
402 assert_eq!(config.strategy_type, RetryStrategyType::Fixed);
403 assert!(!config.retry_on_server_error);
404 }
405
406 #[test]
407 fn test_retry_config_aggressive() {
408 let config = RetryConfig::aggressive();
409 assert_eq!(config.max_retries, 5);
410 assert!(config.retry_on_server_error);
411 assert!(config.retry_on_timeout);
412 }
413
414 #[test]
415 fn test_should_retry_network_error() {
416 let strategy = RetryStrategy::default_strategy();
417 let error = Error::network("Connection failed");
418
419 assert!(strategy.should_retry(&error, 1));
420 assert!(strategy.should_retry(&error, 2));
421 assert!(strategy.should_retry(&error, 3));
422 assert!(!strategy.should_retry(&error, 4));
423 }
424
425 #[test]
426 fn test_should_retry_rate_limit() {
427 let strategy = RetryStrategy::default_strategy();
428 let error = Error::rate_limit("Rate limit exceeded", None);
429
430 assert!(strategy.should_retry(&error, 1));
431 assert!(strategy.should_retry(&error, 3));
432 }
433
434 #[test]
435 fn test_should_not_retry_invalid_request() {
436 let strategy = RetryStrategy::default_strategy();
437 let error = Error::invalid_request("Bad request");
438
439 assert!(!strategy.should_retry(&error, 1));
440 }
441
442 #[test]
443 fn test_calculate_delay_fixed() {
444 let config = RetryConfig {
445 strategy_type: RetryStrategyType::Fixed,
446 base_delay_ms: 1000,
447 jitter_factor: 0.0,
448 ..Default::default()
449 };
450 let strategy = RetryStrategy::new(config);
451 let error = Error::network("test");
452
453 assert_eq!(strategy.calculate_delay(1, &error).as_millis(), 1000);
454 assert_eq!(strategy.calculate_delay(2, &error).as_millis(), 1000);
455 assert_eq!(strategy.calculate_delay(3, &error).as_millis(), 1000);
456 }
457
458 #[test]
459 fn test_calculate_delay_exponential() {
460 let config = RetryConfig {
461 strategy_type: RetryStrategyType::Exponential,
462 base_delay_ms: 100,
463 max_delay_ms: 10000,
464 jitter_factor: 0.0,
465 ..Default::default()
466 };
467 let strategy = RetryStrategy::new(config);
468 let error = Error::network("test");
469
470 assert_eq!(strategy.calculate_delay(1, &error).as_millis(), 100);
471 assert_eq!(strategy.calculate_delay(2, &error).as_millis(), 200);
472 assert_eq!(strategy.calculate_delay(3, &error).as_millis(), 400);
473 assert_eq!(strategy.calculate_delay(4, &error).as_millis(), 800);
474 }
475
476 #[test]
477 fn test_calculate_delay_linear() {
478 let config = RetryConfig {
479 strategy_type: RetryStrategyType::Linear,
480 base_delay_ms: 500,
481 max_delay_ms: 10000,
482 jitter_factor: 0.0,
483 ..Default::default()
484 };
485 let strategy = RetryStrategy::new(config);
486 let error = Error::network("test");
487
488 assert_eq!(strategy.calculate_delay(1, &error).as_millis(), 500);
489 assert_eq!(strategy.calculate_delay(2, &error).as_millis(), 1000);
490 assert_eq!(strategy.calculate_delay(3, &error).as_millis(), 1500);
491 }
492
493 #[test]
494 fn test_calculate_delay_with_max_limit() {
495 let config = RetryConfig {
496 strategy_type: RetryStrategyType::Exponential,
497 base_delay_ms: 1000,
498 max_delay_ms: 5000,
499 jitter_factor: 0.0,
500 ..Default::default()
501 };
502 let strategy = RetryStrategy::new(config);
503 let error = Error::network("test");
504
505 assert_eq!(strategy.calculate_delay(1, &error).as_millis(), 1000);
506 assert_eq!(strategy.calculate_delay(2, &error).as_millis(), 2000);
507 assert_eq!(strategy.calculate_delay(3, &error).as_millis(), 4000);
508 assert_eq!(strategy.calculate_delay(4, &error).as_millis(), 5000);
509 assert_eq!(strategy.calculate_delay(5, &error).as_millis(), 5000);
510 }
511
512 #[test]
513 fn test_is_server_error() {
514 assert!(RetryStrategy::is_server_error("500 Internal Server Error"));
516 assert!(RetryStrategy::is_server_error("502 Bad Gateway"));
517 assert!(RetryStrategy::is_server_error("503 Service Unavailable"));
518 assert!(RetryStrategy::is_server_error("504 Gateway Timeout"));
519 assert!(RetryStrategy::is_server_error("HTTP 500"));
520 assert!(RetryStrategy::is_server_error("Error: 502"));
521 assert!(RetryStrategy::is_server_error("Status 503"));
522 assert!(RetryStrategy::is_server_error("internal server error"));
523 assert!(RetryStrategy::is_server_error("bad gateway"));
524 assert!(RetryStrategy::is_server_error("service unavailable"));
525 assert!(RetryStrategy::is_server_error("gateway timeout"));
526
527 assert!(!RetryStrategy::is_server_error("400 Bad Request"));
529 assert!(!RetryStrategy::is_server_error("404 Not Found"));
530 assert!(!RetryStrategy::is_server_error("200 OK"));
531
532 assert!(!RetryStrategy::is_server_error("order_id: 15001234"));
534 assert!(!RetryStrategy::is_server_error("balance: 5020.50"));
535 assert!(!RetryStrategy::is_server_error("id=25030"));
536 assert!(!RetryStrategy::is_server_error("amount: 5000"));
537 assert!(!RetryStrategy::is_server_error("price: 50200"));
538 assert!(!RetryStrategy::is_server_error("timestamp: 1500123456789"));
539 }
540
541 #[test]
542 fn test_is_server_error_code() {
543 assert!(RetryStrategy::is_server_error_code(500));
545 assert!(RetryStrategy::is_server_error_code(501));
546 assert!(RetryStrategy::is_server_error_code(502));
547 assert!(RetryStrategy::is_server_error_code(503));
548 assert!(RetryStrategy::is_server_error_code(504));
549 assert!(RetryStrategy::is_server_error_code(505));
550 assert!(RetryStrategy::is_server_error_code(599));
551
552 assert!(!RetryStrategy::is_server_error_code(200));
554 assert!(!RetryStrategy::is_server_error_code(201));
555 assert!(!RetryStrategy::is_server_error_code(301));
556 assert!(!RetryStrategy::is_server_error_code(400));
557 assert!(!RetryStrategy::is_server_error_code(401));
558 assert!(!RetryStrategy::is_server_error_code(403));
559 assert!(!RetryStrategy::is_server_error_code(404));
560 assert!(!RetryStrategy::is_server_error_code(429));
561 assert!(!RetryStrategy::is_server_error_code(499));
562 assert!(!RetryStrategy::is_server_error_code(600));
563 assert!(!RetryStrategy::is_server_error_code(0));
564 }
565
566 #[test]
567 fn test_is_server_error_message() {
568 assert!(RetryStrategy::is_server_error_message(
570 "500 Internal Server Error"
571 ));
572 assert!(RetryStrategy::is_server_error_message("502 Bad Gateway"));
573 assert!(RetryStrategy::is_server_error_message(
574 "503 Service Unavailable"
575 ));
576 assert!(RetryStrategy::is_server_error_message(
577 "504 Gateway Timeout"
578 ));
579
580 assert!(RetryStrategy::is_server_error_message(
582 "INTERNAL SERVER ERROR"
583 ));
584 assert!(RetryStrategy::is_server_error_message("Bad Gateway"));
585 assert!(RetryStrategy::is_server_error_message(
586 "SERVICE UNAVAILABLE"
587 ));
588
589 assert!(RetryStrategy::is_server_error_message("Error 500"));
591 assert!(RetryStrategy::is_server_error_message("HTTP/1.1 502"));
592 assert!(RetryStrategy::is_server_error_message("Status: 503"));
593 assert!(RetryStrategy::is_server_error_message("504"));
594
595 assert!(!RetryStrategy::is_server_error_message(
597 "order_id: 15001234"
598 ));
599 assert!(!RetryStrategy::is_server_error_message("balance: 5020.50"));
600 assert!(!RetryStrategy::is_server_error_message("id=25030"));
601 assert!(!RetryStrategy::is_server_error_message("amount: 5000"));
602 assert!(!RetryStrategy::is_server_error_message("price: 50200"));
603 assert!(!RetryStrategy::is_server_error_message(
604 "timestamp: 1500123456789"
605 ));
606 assert!(!RetryStrategy::is_server_error_message("order5020"));
607 assert!(!RetryStrategy::is_server_error_message("5030items"));
608 }
609
610 #[test]
611 fn test_is_timeout_error() {
612 assert!(RetryStrategy::is_timeout_error("Request timeout"));
613 assert!(RetryStrategy::is_timeout_error("Connection timed out"));
614 assert!(RetryStrategy::is_timeout_error("408 Request Timeout"));
615 assert!(!RetryStrategy::is_timeout_error("Connection refused"));
616 }
617
618 #[test]
619 fn test_retry_config_validate_default() {
620 let config = RetryConfig::default();
621 let result = config.validate();
622 assert!(result.is_ok());
623 assert!(result.unwrap().warnings.is_empty());
624 }
625
626 #[test]
627 fn test_retry_config_validate_max_retries_too_high() {
628 let config = RetryConfig {
629 max_retries: 15,
630 ..Default::default()
631 };
632 let result = config.validate();
633 assert!(result.is_err());
634 let err = result.unwrap_err();
635 assert_eq!(err.field_name(), "max_retries");
636 assert!(matches!(
637 err,
638 crate::error::ConfigValidationError::ValueTooHigh { .. }
639 ));
640 }
641
642 #[test]
643 fn test_retry_config_validate_max_retries_boundary() {
644 let config = RetryConfig {
646 max_retries: 10,
647 ..Default::default()
648 };
649 assert!(config.validate().is_ok());
650
651 let config = RetryConfig {
653 max_retries: 11,
654 ..Default::default()
655 };
656 assert!(config.validate().is_err());
657 }
658
659 #[test]
660 fn test_retry_config_validate_base_delay_too_low() {
661 let config = RetryConfig {
662 base_delay_ms: 5,
663 ..Default::default()
664 };
665 let result = config.validate();
666 assert!(result.is_err());
667 let err = result.unwrap_err();
668 assert_eq!(err.field_name(), "base_delay_ms");
669 assert!(matches!(
670 err,
671 crate::error::ConfigValidationError::ValueTooLow { .. }
672 ));
673 }
674
675 #[test]
676 fn test_retry_config_validate_base_delay_boundary() {
677 let config = RetryConfig {
679 base_delay_ms: 10,
680 ..Default::default()
681 };
682 assert!(config.validate().is_ok());
683
684 let config = RetryConfig {
686 base_delay_ms: 9,
687 ..Default::default()
688 };
689 assert!(config.validate().is_err());
690 }
691
692 #[test]
693 fn test_rate_limit_error_minimum_delay() {
694 let config = RetryConfig {
695 strategy_type: RetryStrategyType::Fixed,
696 base_delay_ms: 100, jitter_factor: 0.0,
698 ..Default::default()
699 };
700 let strategy = RetryStrategy::new(config);
701 let error = Error::rate_limit("Rate limit exceeded", None);
702
703 assert!(strategy.calculate_delay(1, &error).as_millis() >= 2000);
704 }
705}