1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_time_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] #![allow(mismatched_lifetime_syntaxes)] #![allow(clippy::collapsible_if)] #![deny(clippy::unused_async)]
47use std::error::Error;
50use std::fmt::{self, Debug, Display, Error as FmtError, Formatter};
51use std::iter;
52use std::time::{Duration, Instant, SystemTime};
53
54#[derive(Debug, Clone)]
66pub struct RetryError<E> {
67 doing: String,
69 errors: Vec<(Attempt, E, Instant)>,
71 n_errors: usize,
76 first_error_at: Option<SystemTime>,
90}
91
92#[derive(Debug, Clone)]
94enum Attempt {
95 Single(usize),
97 Range(usize, usize),
99}
100
101impl<E: Debug + AsRef<dyn Error>> Error for RetryError<E> {}
104
105impl<E> RetryError<E> {
106 pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
117 RetryError {
118 doing: doing.into(),
119 errors: Vec::new(),
120 n_errors: 0,
121 first_error_at: None,
122 }
123 }
124 pub fn push_timed<T>(&mut self, err: T, instant: Instant, wall_clock: Option<SystemTime>)
145 where
146 T: Into<E>,
147 {
148 if self.n_errors < usize::MAX {
149 self.n_errors += 1;
150 let attempt = Attempt::Single(self.n_errors);
151
152 if self.first_error_at.is_none() {
153 self.first_error_at = wall_clock;
154 }
155
156 self.errors.push((attempt, err.into(), instant));
157 }
158 }
159
160 pub fn push<T>(&mut self, err: T)
169 where
170 T: Into<E>,
171 {
172 self.push_timed(err, Instant::now(), Some(SystemTime::now()));
173 }
174
175 pub fn sources(&self) -> impl Iterator<Item = &E> {
178 self.errors.iter().map(|(.., e, _)| e)
179 }
180
181 pub fn len(&self) -> usize {
183 self.errors.len()
184 }
185
186 pub fn is_empty(&self) -> bool {
188 self.errors.is_empty()
189 }
190
191 #[allow(clippy::disallowed_methods)] pub fn extend<T>(&mut self, iter: impl IntoIterator<Item = T>)
206 where
207 T: Into<E>,
208 {
209 for item in iter {
210 self.push(item);
211 }
212 }
213
214 pub fn dedup_by<F>(&mut self, same_err: F)
219 where
220 F: Fn(&E, &E) -> bool,
221 {
222 let mut old_errs = Vec::new();
223 std::mem::swap(&mut old_errs, &mut self.errors);
224
225 for (attempt, err, timestamp) in old_errs {
226 if let Some((last_attempt, last_err, ..)) = self.errors.last_mut() {
227 if same_err(last_err, &err) {
228 last_attempt.grow();
229 } else {
230 self.errors.push((attempt, err, timestamp));
231 }
232 } else {
233 self.errors.push((attempt, err, timestamp));
234 }
235 }
236 }
237
238 pub fn extend_from_retry_error(&mut self, other: RetryError<E>) {
244 if self.first_error_at.is_none() {
245 self.first_error_at = other.first_error_at;
246 }
247
248 for (attempt, err, timestamp) in other.errors {
249 let new_attempt = match attempt {
250 Attempt::Single(_) => {
251 let Some(new_n_errors) = self.n_errors.checked_add(1) else {
252 break;
253 };
254 self.n_errors = new_n_errors;
255 Attempt::Single(new_n_errors)
256 }
257 Attempt::Range(first, last) => {
258 let count = last - first + 1;
259 let Some(new_n_errors) = self.n_errors.checked_add(count) else {
260 break;
261 };
262 let start = self.n_errors + 1;
263 self.n_errors = new_n_errors;
264 Attempt::Range(start, new_n_errors)
265 }
266 };
267
268 self.errors.push((new_attempt, err, timestamp));
269 }
270 }
271}
272
273impl<E: PartialEq<E>> RetryError<E> {
274 pub fn dedup(&mut self) {
277 self.dedup_by(PartialEq::eq);
278 }
279}
280
281impl Attempt {
282 fn grow(&mut self) {
284 *self = match *self {
285 Attempt::Single(idx) => Attempt::Range(idx, idx + 1),
286 Attempt::Range(first, last) => Attempt::Range(first, last + 1),
287 };
288 }
289}
290
291impl<E> IntoIterator for RetryError<E> {
292 type Item = E;
293 type IntoIter = std::vec::IntoIter<E>;
294 #[allow(clippy::needless_collect)]
295 fn into_iter(self) -> Self::IntoIter {
300 self.errors
301 .into_iter()
302 .map(|(.., e, _)| e)
303 .collect::<Vec<_>>()
304 .into_iter()
305 }
306}
307
308impl Display for Attempt {
309 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
310 match self {
311 Attempt::Single(idx) => write!(f, "Attempt {}", idx),
312 Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
313 }
314 }
315}
316
317impl<E: AsRef<dyn Error>> Display for RetryError<E> {
318 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
319 let show_timestamps = f.alternate();
320
321 match self.n_errors {
322 0 => write!(f, "Unable to {}. (No errors given)", self.doing),
323 1 => {
324 write!(f, "Unable to {}", self.doing)?;
325
326 if show_timestamps {
327 if let (Some((.., timestamp)), Some(first_at)) =
328 (self.errors.first(), self.first_error_at)
329 {
330 write!(
331 f,
332 " at {} ({})",
333 humantime::format_rfc3339(first_at),
334 FormatTimeAgo(timestamp.elapsed())
335 )?;
336 }
337 }
338
339 write!(f, ": ")?;
340 fmt_error_with_sources(self.errors[0].1.as_ref(), f)
341 }
342 n => {
343 write!(
344 f,
345 "Tried to {} {} times, but all attempts failed",
346 self.doing, n
347 )?;
348
349 if show_timestamps {
350 if let (Some(first_at), Some((.., first_ts)), Some((.., last_ts))) =
351 (self.first_error_at, self.errors.first(), self.errors.last())
352 {
353 let duration = last_ts.saturating_duration_since(*first_ts);
354
355 write!(f, " (from {} ", humantime::format_rfc3339(first_at))?;
356
357 if duration.as_secs() > 0 {
358 write!(f, "to {}", humantime::format_rfc3339(first_at + duration))?;
359 }
360
361 write!(f, ", {})", FormatTimeAgo(last_ts.elapsed()))?;
362 }
363 }
364
365 let first_ts = self.errors.first().map(|(.., ts)| ts);
366 for (attempt, e, timestamp) in &self.errors {
367 write!(f, "\n{}", attempt)?;
368
369 if show_timestamps {
370 if let Some(first_ts) = first_ts {
371 let offset = timestamp.saturating_duration_since(*first_ts);
372 if offset.as_secs() > 0 {
373 write!(f, " (+{})", FormatDuration(offset))?;
374 }
375 }
376 }
377
378 write!(f, ": ")?;
379 fmt_error_with_sources(e.as_ref(), f)?;
380 }
381 Ok(())
382 }
383 }
384 }
385}
386
387struct FormatDuration(Duration);
392
393impl Display for FormatDuration {
394 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
395 fmt_duration_impl(self.0, f)
396 }
397}
398
399struct FormatTimeAgo(Duration);
401
402impl Display for FormatTimeAgo {
403 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
404 let secs = self.0.as_secs();
405 let millis = self.0.as_millis();
406
407 if secs == 0 && millis == 0 {
409 return write!(f, "just now");
410 }
411
412 fmt_duration_impl(self.0, f)?;
413 write!(f, " ago")
414 }
415}
416
417fn fmt_duration_impl(duration: Duration, f: &mut Formatter<'_>) -> fmt::Result {
422 let secs = duration.as_secs();
423
424 if secs == 0 {
425 let millis = duration.as_millis();
426 if millis == 0 {
427 write!(f, "0s")
428 } else {
429 write!(f, "{}ms", millis)
430 }
431 } else if secs < 60 {
432 write!(f, "{}s", secs)
433 } else if secs < 3600 {
434 let mins = secs / 60;
435 let rem_secs = secs % 60;
436 if rem_secs == 0 {
437 write!(f, "{}m", mins)
438 } else {
439 write!(f, "{}m {}s", mins, rem_secs)
440 }
441 } else {
442 let hours = secs / 3600;
443 let mins = (secs % 3600) / 60;
444 if mins == 0 {
445 write!(f, "{}h", hours)
446 } else {
447 write!(f, "{}h {}m", hours, mins)
448 }
449 }
450}
451
452pub fn fmt_error_with_sources(mut e: &dyn Error, f: &mut fmt::Formatter) -> fmt::Result {
493 let mut last = String::new();
498 let mut sep = iter::once("").chain(iter::repeat(": "));
499
500 loop {
504 let this = e.to_string();
505 if !last.contains(&this) {
506 write!(f, "{}{}", sep.next().expect("repeat ended"), &this)?;
507 }
508 last = this;
509
510 if let Some(ne) = e.source() {
511 e = ne;
512 } else {
513 break;
514 }
515 }
516 Ok(())
517}
518
519#[cfg(test)]
520mod test {
521 #![allow(clippy::bool_assert_comparison)]
523 #![allow(clippy::clone_on_copy)]
524 #![allow(clippy::dbg_macro)]
525 #![allow(clippy::mixed_attributes_style)]
526 #![allow(clippy::print_stderr)]
527 #![allow(clippy::print_stdout)]
528 #![allow(clippy::single_char_pattern)]
529 #![allow(clippy::unwrap_used)]
530 #![allow(clippy::unchecked_time_subtraction)]
531 #![allow(clippy::useless_vec)]
532 #![allow(clippy::needless_pass_by_value)]
533 #![allow(clippy::disallowed_methods)]
535 use super::*;
536 use derive_more::From;
537
538 #[test]
539 fn bad_parse1() {
540 let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
541 if let Err(e) = "maybe".parse::<bool>() {
542 err.push(e);
543 }
544 if let Err(e) = "a few".parse::<u32>() {
545 err.push(e);
546 }
547 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
548 err.push(e);
549 }
550
551 let disp = format!("{}", err);
552 assert_eq!(
553 disp,
554 "\
555Tried to convert some things 3 times, but all attempts failed
556Attempt 1: provided string was not `true` or `false`
557Attempt 2: invalid digit found in string
558Attempt 3: invalid IP address syntax"
559 );
560
561 let disp_alt = format!("{:#}", err);
562 assert!(disp_alt.contains("Tried to convert some things 3 times, but all attempts failed"));
563 assert!(disp_alt.contains("(from 20")); }
565
566 #[test]
567 fn no_problems() {
568 let empty: RetryError<anyhow::Error> =
569 RetryError::in_attempt_to("immanentize the eschaton");
570 let disp = format!("{}", empty);
571 assert_eq!(
572 disp,
573 "Unable to immanentize the eschaton. (No errors given)"
574 );
575 }
576
577 #[test]
578 fn one_problem() {
579 let mut err: RetryError<anyhow::Error> =
580 RetryError::in_attempt_to("connect to torproject.org");
581 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
582 err.push(e);
583 }
584 let disp = format!("{}", err);
585 assert_eq!(
586 disp,
587 "Unable to connect to torproject.org: invalid IP address syntax"
588 );
589
590 let disp_alt = format!("{:#}", err);
591 assert!(disp_alt.contains("Unable to connect to torproject.org at 20")); assert!(disp_alt.contains("invalid IP address syntax"));
593 }
594
595 #[test]
596 fn operations() {
597 use std::num::ParseIntError;
598
599 #[derive(From, Clone, Debug, Eq, PartialEq)]
600 struct Wrapper(ParseIntError);
601
602 impl AsRef<dyn Error + 'static> for Wrapper {
603 fn as_ref(&self) -> &(dyn Error + 'static) {
604 &self.0
605 }
606 }
607
608 let mut err: RetryError<Wrapper> = RetryError::in_attempt_to("parse some integers");
609 assert!(err.is_empty());
610 assert_eq!(err.len(), 0);
611 err.extend(
612 vec!["not", "your", "number"]
613 .iter()
614 .filter_map(|s| s.parse::<u16>().err())
615 .map(Wrapper),
616 );
617 assert!(!err.is_empty());
618 assert_eq!(err.len(), 3);
619
620 let cloned = err.clone();
621 for (s1, s2) in err.sources().zip(cloned.sources()) {
622 assert_eq!(s1, s2);
623 }
624
625 err.dedup();
626
627 let disp = format!("{}", err);
628 assert_eq!(
629 disp,
630 "\
631Tried to parse some integers 3 times, but all attempts failed
632Attempts 1..3: invalid digit found in string"
633 );
634
635 let disp_alt = format!("{:#}", err);
636 assert!(disp_alt.contains("Tried to parse some integers 3 times, but all attempts failed"));
637 assert!(disp_alt.contains("(from 20")); }
639
640 #[test]
641 fn overflow() {
642 use std::num::ParseIntError;
643 let mut err: RetryError<ParseIntError> =
644 RetryError::in_attempt_to("parse too many integers");
645 assert!(err.is_empty());
646 let mut errors: Vec<ParseIntError> = vec!["no", "numbers"]
647 .iter()
648 .filter_map(|s| s.parse::<u16>().err())
649 .collect();
650 err.n_errors = usize::MAX;
651 err.errors.push((
652 Attempt::Range(1, err.n_errors),
653 errors.pop().expect("parser did not fail"),
654 Instant::now(),
655 ));
656 assert!(err.n_errors == usize::MAX);
657 assert!(err.len() == 1);
658
659 err.push(errors.pop().expect("parser did not fail"));
660 assert!(err.n_errors == usize::MAX);
661 assert!(err.len() == 1);
662 }
663
664 #[test]
665 fn extend_from_retry_preserve_timestamps() {
666 let n1 = Instant::now();
667 let n2 = n1 + Duration::from_secs(10);
668 let n3 = n1 + Duration::from_secs(20);
669
670 let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do first thing");
671 let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do second thing");
672
673 err2.push_timed(anyhow::Error::msg("e1"), n1, None);
674 err2.push_timed(anyhow::Error::msg("e2"), n2, None);
675
676 assert!(err1.first_error_at.is_none());
678
679 err1.extend_from_retry_error(err2);
680
681 assert_eq!(err1.len(), 2);
682 assert_eq!(err1.errors[0].2, n1);
684 assert_eq!(err1.errors[1].2, n2);
685
686 err1.push_timed(anyhow::Error::msg("e3"), n3, None);
688 assert_eq!(err1.len(), 3);
689 assert_eq!(err1.errors[2].2, n3);
690 }
691
692 #[test]
693 fn extend_from_retry_preserve_ranges() {
694 let n1 = Instant::now();
695 let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 1");
696
697 err1.push(anyhow::Error::msg("e1"));
699 err1.push(anyhow::Error::msg("e2"));
700 assert_eq!(err1.n_errors, 2);
701
702 let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 2");
703 let _err_msg = anyhow::Error::msg("repeated");
705 err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
706 err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
707 err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
708
709 err2.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
711 assert_eq!(err2.len(), 1); match err2.errors[0].0 {
713 Attempt::Range(1, 3) => {}
714 _ => panic!("Expected range 1..3"),
715 }
716
717 err1.extend_from_retry_error(err2);
719
720 assert_eq!(err1.len(), 3); assert_eq!(err1.n_errors, 5); match err1.errors[2].0 {
725 Attempt::Range(3, 5) => {}
726 ref x => panic!("Expected range 3..5, got {:?}", x),
727 }
728 }
729}