exiftool_rs_wrapper/
retry.rs1use crate::error::Error;
6use std::time::Duration;
7
8#[derive(Debug, Clone)]
10pub struct RetryPolicy {
11 pub max_attempts: u32,
13 pub initial_delay: Duration,
15 pub backoff_multiplier: f64,
17 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 pub fn new(max_attempts: u32) -> Self {
35 Self {
36 max_attempts,
37 ..Default::default()
38 }
39 }
40
41 pub fn initial_delay(mut self, delay: Duration) -> Self {
43 self.initial_delay = delay;
44 self
45 }
46
47 pub fn backoff(mut self, multiplier: f64) -> Self {
49 self.backoff_multiplier = multiplier;
50 self
51 }
52
53 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 pub fn should_retry(&self, attempt: u32, error: &Error) -> bool {
68 if attempt >= self.max_attempts {
69 return false;
70 }
71
72 matches!(
74 error,
75 Error::Io(_) | Error::Timeout | Error::Process { .. } | Error::MutexPoisoned
76 )
77 }
78}
79
80#[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
108pub 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#[derive(Debug)]
136pub struct BatchResult<T, E> {
137 pub successes: Vec<T>,
139 pub failures: Vec<E>,
141 pub total: usize,
143}
144
145impl<T, E> BatchResult<T, E> {
146 pub fn new() -> Self {
148 Self {
149 successes: Vec::new(),
150 failures: Vec::new(),
151 total: 0,
152 }
153 }
154
155 pub fn add_success(&mut self, item: T) {
157 self.successes.push(item);
158 self.total += 1;
159 }
160
161 pub fn add_failure(&mut self, error: E) {
163 self.failures.push(error);
164 self.total += 1;
165 }
166
167 pub fn is_complete(&self) -> bool {
169 self.failures.is_empty()
170 }
171
172 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 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
197pub trait Recoverable {
199 fn is_recoverable(&self) -> bool;
201
202 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 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 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 assert_eq!(attempts.get(), 1);
308 }
309}