Skip to main content

voirs_cli/workflow/
retry.rs

1//! Retry Logic Module
2//!
3//! Provides retry strategies with configurable backoff algorithms.
4
5use super::definition::{BackoffType, RetryStrategy};
6use serde::{Deserialize, Serialize};
7
8/// Retry configuration
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RetryConfig {
11    /// Maximum attempts
12    pub max_attempts: usize,
13    /// Initial delay in milliseconds
14    pub initial_delay_ms: u64,
15    /// Maximum delay in milliseconds
16    pub max_delay_ms: u64,
17    /// Backoff multiplier
18    pub multiplier: f64,
19    /// Backoff strategy
20    pub strategy: BackoffStrategy,
21}
22
23impl Default for RetryConfig {
24    fn default() -> Self {
25        Self {
26            max_attempts: 3,
27            initial_delay_ms: 1000,
28            max_delay_ms: 60_000,
29            multiplier: 2.0,
30            strategy: BackoffStrategy::Exponential,
31        }
32    }
33}
34
35/// Backoff strategy enum
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37pub enum BackoffStrategy {
38    /// Fixed delay
39    Fixed,
40    /// Linear backoff
41    Linear,
42    /// Exponential backoff
43    Exponential,
44    /// Exponential with jitter
45    ExponentialJitter,
46}
47
48/// Retry manager
49pub struct RetryManager;
50
51impl RetryManager {
52    /// Create new retry manager
53    pub fn new() -> Self {
54        Self
55    }
56
57    /// Calculate delay for retry attempt
58    pub fn calculate_delay(&self, strategy: &RetryStrategy, attempt: usize) -> u64 {
59        let backoff_strategy = match strategy.backoff {
60            BackoffType::Fixed => BackoffStrategy::Fixed,
61            BackoffType::Linear => BackoffStrategy::Linear,
62            BackoffType::Exponential => BackoffStrategy::Exponential,
63            BackoffType::ExponentialJitter => BackoffStrategy::ExponentialJitter,
64        };
65
66        let delay = match backoff_strategy {
67            BackoffStrategy::Fixed => strategy.initial_delay_ms,
68            BackoffStrategy::Linear => strategy.initial_delay_ms * attempt as u64,
69            BackoffStrategy::Exponential => {
70                let delay = strategy.initial_delay_ms
71                    * (strategy.backoff_multiplier.powi(attempt as i32 - 1) as u64);
72                delay.min(strategy.max_delay_ms)
73            }
74            BackoffStrategy::ExponentialJitter => {
75                let base_delay = strategy.initial_delay_ms
76                    * (strategy.backoff_multiplier.powi(attempt as i32 - 1) as u64);
77                // Simple jitter: add 10% of base delay
78                let jitter = (base_delay as f64 * 0.1) as u64;
79                (base_delay + jitter).min(strategy.max_delay_ms)
80            }
81        };
82
83        delay
84            .max(strategy.initial_delay_ms)
85            .min(strategy.max_delay_ms)
86    }
87
88    /// Check if should retry
89    pub fn should_retry(&self, attempt: usize, max_attempts: usize) -> bool {
90        attempt < max_attempts
91    }
92}
93
94impl Default for RetryManager {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_retry_config_default() {
106        let config = RetryConfig::default();
107        assert_eq!(config.max_attempts, 3);
108        assert_eq!(config.initial_delay_ms, 1000);
109        assert_eq!(config.max_delay_ms, 60_000);
110    }
111
112    #[test]
113    fn test_retry_manager_creation() {
114        let manager = RetryManager::new();
115        // Just verify creation works
116        assert!(manager.should_retry(1, 3));
117    }
118
119    #[test]
120    fn test_should_retry() {
121        let manager = RetryManager::new();
122        assert!(manager.should_retry(1, 3));
123        assert!(manager.should_retry(2, 3));
124        assert!(!manager.should_retry(3, 3));
125        assert!(!manager.should_retry(4, 3));
126    }
127
128    #[test]
129    fn test_fixed_backoff() {
130        let manager = RetryManager::new();
131        let strategy = RetryStrategy {
132            max_attempts: 3,
133            backoff: BackoffType::Fixed,
134            initial_delay_ms: 1000,
135            max_delay_ms: 60_000,
136            backoff_multiplier: 2.0,
137        };
138
139        let delay1 = manager.calculate_delay(&strategy, 1);
140        let delay2 = manager.calculate_delay(&strategy, 2);
141        let delay3 = manager.calculate_delay(&strategy, 3);
142
143        assert_eq!(delay1, 1000);
144        assert_eq!(delay2, 1000);
145        assert_eq!(delay3, 1000);
146    }
147
148    #[test]
149    fn test_linear_backoff() {
150        let manager = RetryManager::new();
151        let strategy = RetryStrategy {
152            max_attempts: 3,
153            backoff: BackoffType::Linear,
154            initial_delay_ms: 1000,
155            max_delay_ms: 60_000,
156            backoff_multiplier: 2.0,
157        };
158
159        let delay1 = manager.calculate_delay(&strategy, 1);
160        let delay2 = manager.calculate_delay(&strategy, 2);
161        let delay3 = manager.calculate_delay(&strategy, 3);
162
163        assert_eq!(delay1, 1000);
164        assert_eq!(delay2, 2000);
165        assert_eq!(delay3, 3000);
166    }
167
168    #[test]
169    fn test_exponential_backoff() {
170        let manager = RetryManager::new();
171        let strategy = RetryStrategy {
172            max_attempts: 4,
173            backoff: BackoffType::Exponential,
174            initial_delay_ms: 1000,
175            max_delay_ms: 60_000,
176            backoff_multiplier: 2.0,
177        };
178
179        let delay1 = manager.calculate_delay(&strategy, 1);
180        let delay2 = manager.calculate_delay(&strategy, 2);
181        let delay3 = manager.calculate_delay(&strategy, 3);
182
183        assert_eq!(delay1, 1000);
184        assert_eq!(delay2, 2000);
185        assert_eq!(delay3, 4000);
186    }
187
188    #[test]
189    fn test_max_delay_cap() {
190        let manager = RetryManager::new();
191        let strategy = RetryStrategy {
192            max_attempts: 10,
193            backoff: BackoffType::Exponential,
194            initial_delay_ms: 1000,
195            max_delay_ms: 5000,
196            backoff_multiplier: 2.0,
197        };
198
199        let delay5 = manager.calculate_delay(&strategy, 5);
200        assert!(delay5 <= 5000);
201    }
202}