Skip to main content

exiftool_rs_wrapper/
retry.rs

1//! 错误恢复和重试机制
2//!
3//! 提供错误恢复策略、重试机制、部分成功处理
4
5use crate::error::Error;
6use std::time::Duration;
7
8/// 重试策略
9#[derive(Debug, Clone)]
10pub struct RetryPolicy {
11    /// 最大重试次数
12    pub max_attempts: u32,
13    /// 初始延迟
14    pub initial_delay: Duration,
15    /// 延迟倍数(指数退避)
16    pub backoff_multiplier: f64,
17    /// 最大延迟
18    pub max_delay: Duration,
19}
20
21impl Default for RetryPolicy {
22    fn default() -> Self {
23        Self {
24            max_attempts: 3,
25            initial_delay: Duration::from_millis(100),
26            backoff_multiplier: 2.0,
27            max_delay: Duration::from_secs(30),
28        }
29    }
30}
31
32impl RetryPolicy {
33    /// 创建新的重试策略
34    pub fn new(max_attempts: u32) -> Self {
35        Self {
36            max_attempts,
37            ..Default::default()
38        }
39    }
40
41    /// 设置初始延迟
42    pub fn initial_delay(mut self, delay: Duration) -> Self {
43        self.initial_delay = delay;
44        self
45    }
46
47    /// 设置退避倍数
48    pub fn backoff(mut self, multiplier: f64) -> Self {
49        self.backoff_multiplier = multiplier;
50        self
51    }
52
53    /// 计算第 n 次重试的延迟
54    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
55        if attempt == 0 {
56            return Duration::ZERO;
57        }
58
59        let delay_ms = self.initial_delay.as_millis() as f64
60            * self.backoff_multiplier.powi(attempt as i32 - 1);
61        let delay_ms = delay_ms.min(self.max_delay.as_millis() as f64) as u64;
62
63        Duration::from_millis(delay_ms)
64    }
65
66    /// 检查是否应该重试
67    pub fn should_retry(&self, attempt: u32, error: &Error) -> bool {
68        if attempt >= self.max_attempts {
69            return false;
70        }
71
72        // 只对特定错误进行重试
73        matches!(
74            error,
75            Error::Io(_) | Error::Timeout | Error::Process { .. } | Error::MutexPoisoned
76        )
77    }
78}
79
80/// 执行带重试的操作(需要启用 async 特性)
81#[cfg(feature = "async")]
82pub async fn with_retry<F, Fut, T>(policy: &RetryPolicy, operation: F) -> crate::error::Result<T>
83where
84    F: Fn() -> Fut,
85    Fut: std::future::Future<Output = crate::error::Result<T>>,
86{
87    let mut attempt = 0;
88
89    loop {
90        match operation().await {
91            Ok(result) => return Ok(result),
92            Err(error) => {
93                if !policy.should_retry(attempt, &error) {
94                    return Err(error);
95                }
96
97                attempt += 1;
98                let delay = policy.delay_for_attempt(attempt);
99
100                if delay > Duration::ZERO {
101                    tokio::time::sleep(delay).await;
102                }
103            }
104        }
105    }
106}
107
108/// 同步版本的重试
109pub fn with_retry_sync<F, T>(policy: &RetryPolicy, operation: F) -> crate::error::Result<T>
110where
111    F: Fn() -> crate::error::Result<T>,
112{
113    let mut attempt = 0;
114
115    loop {
116        match operation() {
117            Ok(result) => return Ok(result),
118            Err(error) => {
119                if !policy.should_retry(attempt, &error) {
120                    return Err(error);
121                }
122
123                attempt += 1;
124                let delay = policy.delay_for_attempt(attempt);
125
126                if delay > Duration::ZERO {
127                    std::thread::sleep(delay);
128                }
129            }
130        }
131    }
132}
133
134/// 批量操作结果(支持部分成功)
135#[derive(Debug)]
136pub struct BatchResult<T, E> {
137    /// 成功的结果
138    pub successes: Vec<T>,
139    /// 失败的错误
140    pub failures: Vec<E>,
141    /// 总数量
142    pub total: usize,
143}
144
145impl<T, E> BatchResult<T, E> {
146    /// 创建新的批量结果
147    pub fn new() -> Self {
148        Self {
149            successes: Vec::new(),
150            failures: Vec::new(),
151            total: 0,
152        }
153    }
154
155    /// 添加成功项
156    pub fn add_success(&mut self, item: T) {
157        self.successes.push(item);
158        self.total += 1;
159    }
160
161    /// 添加失败项
162    pub fn add_failure(&mut self, error: E) {
163        self.failures.push(error);
164        self.total += 1;
165    }
166
167    /// 检查是否全部成功
168    pub fn is_complete(&self) -> bool {
169        self.failures.is_empty()
170    }
171
172    /// 获取成功率
173    pub fn success_rate(&self) -> f64 {
174        if self.total == 0 {
175            0.0
176        } else {
177            (self.successes.len() as f64 / self.total as f64) * 100.0
178        }
179    }
180
181    /// 获取失败率
182    pub fn failure_rate(&self) -> f64 {
183        if self.total == 0 {
184            0.0
185        } else {
186            (self.failures.len() as f64 / self.total as f64) * 100.0
187        }
188    }
189}
190
191impl<T, E> Default for BatchResult<T, E> {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197/// 可恢复的错误 trait
198pub trait Recoverable {
199    /// 检查是否可恢复
200    fn is_recoverable(&self) -> bool;
201
202    /// 获取恢复建议
203    fn recovery_suggestion(&self) -> Option<String>;
204}
205
206impl Recoverable for Error {
207    fn is_recoverable(&self) -> bool {
208        matches!(
209            self,
210            Error::Io(_) | Error::Timeout | Error::Process { .. } | Error::MutexPoisoned
211        )
212    }
213
214    fn recovery_suggestion(&self) -> Option<String> {
215        match self {
216            Error::Io(e) if e.kind() == std::io::ErrorKind::NotFound => {
217                Some("请检查文件路径是否正确".to_string())
218            }
219            Error::Timeout => Some("请增加超时时间或检查网络连接".to_string()),
220            Error::ExifToolNotFound => Some("请安装 ExifTool 并添加到 PATH".to_string()),
221            Error::MutexPoisoned => Some("内部错误,请重新创建 ExifTool 实例".to_string()),
222            _ => None,
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_retry_policy() {
233        let policy = RetryPolicy::default();
234
235        assert_eq!(policy.delay_for_attempt(0), Duration::ZERO);
236        assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(100));
237        assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(200));
238    }
239
240    #[test]
241    fn test_retry_policy_builder() {
242        let policy = RetryPolicy::new(5)
243            .initial_delay(Duration::from_secs(1))
244            .backoff(3.0);
245
246        assert_eq!(policy.max_attempts, 5);
247        assert_eq!(policy.initial_delay, Duration::from_secs(1));
248        assert_eq!(policy.backoff_multiplier, 3.0);
249    }
250
251    #[test]
252    fn test_batch_result() {
253        let mut result: BatchResult<i32, String> = BatchResult::new();
254
255        result.add_success(1);
256        result.add_success(2);
257        result.add_failure("error".to_string());
258
259        assert_eq!(result.total, 3);
260        assert_eq!(result.successes.len(), 2);
261        assert_eq!(result.failures.len(), 1);
262        assert!(!result.is_complete());
263        assert!((result.success_rate() - 66.66666666666667).abs() < 1e-10);
264    }
265
266    #[test]
267    fn test_recoverable() {
268        let timeout_err = Error::Timeout;
269        assert!(timeout_err.is_recoverable());
270        assert!(timeout_err.recovery_suggestion().is_some());
271
272        let tag_err = Error::TagNotFound("test".to_string());
273        assert!(!tag_err.is_recoverable());
274    }
275
276    #[test]
277    fn test_retry_sync() {
278        use std::cell::Cell;
279        let policy = RetryPolicy::new(2);
280        let attempts = Cell::new(0);
281
282        // 测试成功的情况
283        let result = with_retry_sync(&policy, || {
284            attempts.set(attempts.get() + 1);
285            Ok(42)
286        });
287
288        assert!(result.is_ok());
289        assert_eq!(result.unwrap(), 42);
290        assert_eq!(attempts.get(), 1);
291    }
292
293    #[test]
294    fn test_retry_sync_failure() {
295        use std::cell::Cell;
296        let policy = RetryPolicy::new(2);
297        let attempts = Cell::new(0);
298
299        // 测试失败的情况
300        let result: Result<i32, _> = with_retry_sync(&policy, || {
301            attempts.set(attempts.get() + 1);
302            Err(Error::TagNotFound("test".to_string()))
303        });
304
305        assert!(result.is_err());
306        // TagNotFound 不可恢复,所以只尝试一次
307        assert_eq!(attempts.get(), 1);
308    }
309}