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, SystemTime};
53
54#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
55use web_time::Instant;
56
57#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
58use std::time::Instant;
59
60#[derive(Debug, Clone)]
72pub struct RetryError<E> {
73 doing: String,
75 errors: Vec<(Attempt, E, Instant)>,
77 n_errors: usize,
82 first_error_at: Option<SystemTime>,
96}
97
98#[derive(Debug, Clone)]
100enum Attempt {
101 Single(usize),
103 Range(usize, usize),
105}
106
107impl<E: Debug + AsRef<dyn Error>> Error for RetryError<E> {}
110
111impl<E> RetryError<E> {
112 pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
123 RetryError {
124 doing: doing.into(),
125 errors: Vec::new(),
126 n_errors: 0,
127 first_error_at: None,
128 }
129 }
130 pub fn push_timed<T>(&mut self, err: T, instant: Instant, wall_clock: Option<SystemTime>)
152 where
153 T: Into<E>,
154 {
155 if self.n_errors < usize::MAX {
156 self.n_errors += 1;
157 let attempt = Attempt::Single(self.n_errors);
158
159 if self.first_error_at.is_none() {
160 self.first_error_at = wall_clock;
161 }
162
163 self.errors.push((attempt, err.into(), instant));
164 }
165 }
166
167 pub fn push<T>(&mut self, err: T)
176 where
177 T: Into<E>,
178 {
179 self.push_timed(err, current_instant(), Some(current_system_time()));
180 }
181
182 pub fn sources(&self) -> impl Iterator<Item = &E> {
185 self.errors.iter().map(|(.., e, _)| e)
186 }
187
188 pub fn len(&self) -> usize {
190 self.errors.len()
191 }
192
193 pub fn is_empty(&self) -> bool {
195 self.errors.is_empty()
196 }
197
198 #[allow(clippy::disallowed_methods)] pub fn extend<T>(&mut self, iter: impl IntoIterator<Item = T>)
213 where
214 T: Into<E>,
215 {
216 for item in iter {
217 self.push(item);
218 }
219 }
220
221 pub fn dedup_by<F>(&mut self, same_err: F)
226 where
227 F: Fn(&E, &E) -> bool,
228 {
229 let mut old_errs = Vec::new();
230 std::mem::swap(&mut old_errs, &mut self.errors);
231
232 for (attempt, err, timestamp) in old_errs {
233 if let Some((last_attempt, last_err, ..)) = self.errors.last_mut() {
234 if same_err(last_err, &err) {
235 last_attempt.grow(attempt.count());
236 } else {
237 self.errors.push((attempt, err, timestamp));
238 }
239 } else {
240 self.errors.push((attempt, err, timestamp));
241 }
242 }
243 }
244
245 pub fn extend_from_retry_error(&mut self, other: RetryError<E>) {
251 if self.first_error_at.is_none() {
252 self.first_error_at = other.first_error_at;
253 }
254
255 for (attempt, err, timestamp) in other.errors {
256 let Some(new_n_errors) = self.n_errors.checked_add(attempt.count()) else {
257 break;
258 };
259
260 let new_attempt = match attempt {
261 Attempt::Single(_) => Attempt::Single(new_n_errors),
262 Attempt::Range(_, _) => Attempt::Range(self.n_errors + 1, new_n_errors),
263 };
264
265 self.errors.push((new_attempt, err, timestamp));
266 self.n_errors = new_n_errors;
267 }
268 }
269}
270
271impl<E: PartialEq<E>> RetryError<E> {
272 pub fn dedup(&mut self) {
275 self.dedup_by(PartialEq::eq);
276 }
277}
278
279impl Attempt {
280 fn grow(&mut self, count: usize) {
282 *self = match *self {
283 Attempt::Single(idx) => Attempt::Range(idx, idx + count),
284 Attempt::Range(first, last) => Attempt::Range(first, last + count),
285 };
286 }
287
288 fn count(&self) -> usize {
290 match *self {
291 Attempt::Single(_) => 1,
292 Attempt::Range(first, last) => last - first + 1,
293 }
294 }
295}
296
297impl<E> IntoIterator for RetryError<E> {
298 type Item = E;
299 type IntoIter = std::vec::IntoIter<E>;
300 #[allow(clippy::needless_collect)]
301 fn into_iter(self) -> Self::IntoIter {
306 self.errors
307 .into_iter()
308 .map(|(.., e, _)| e)
309 .collect::<Vec<_>>()
310 .into_iter()
311 }
312}
313
314impl Display for Attempt {
315 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
316 match self {
317 Attempt::Single(idx) => write!(f, "Attempt {}", idx),
318 Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
319 }
320 }
321}
322
323impl<E: AsRef<dyn Error>> Display for RetryError<E> {
324 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
325 let show_timestamps = f.alternate();
326
327 match self.n_errors {
328 0 => write!(f, "Unable to {}. (No errors given)", self.doing),
329 1 => {
330 write!(f, "Unable to {}", self.doing)?;
331
332 if show_timestamps {
333 if let (Some((.., timestamp)), Some(first_at)) =
334 (self.errors.first(), self.first_error_at)
335 {
336 write!(
337 f,
338 " at {} ({})",
339 humantime::format_rfc3339(first_at),
340 FormatTimeAgo(timestamp.elapsed())
341 )?;
342 }
343 }
344
345 write!(f, ": ")?;
346 fmt_error_with_sources(self.errors[0].1.as_ref(), f)
347 }
348 n => {
349 write!(
350 f,
351 "Tried to {} {} times, but all attempts failed",
352 self.doing, n
353 )?;
354
355 if show_timestamps {
356 if let (Some(first_at), Some((.., first_ts)), Some((.., last_ts))) =
357 (self.first_error_at, self.errors.first(), self.errors.last())
358 {
359 let duration = last_ts.saturating_duration_since(*first_ts);
360
361 write!(f, " (from {} ", humantime::format_rfc3339(first_at))?;
362
363 if duration.as_secs() > 0 {
364 write!(f, "to {}", humantime::format_rfc3339(first_at + duration))?;
365 }
366
367 write!(f, ", {})", FormatTimeAgo(last_ts.elapsed()))?;
368 }
369 }
370
371 let first_ts = self.errors.first().map(|(.., ts)| ts);
372 for (attempt, e, timestamp) in &self.errors {
373 write!(f, "\n{}", attempt)?;
374
375 if show_timestamps {
376 if let Some(first_ts) = first_ts {
377 let offset = timestamp.saturating_duration_since(*first_ts);
378 if offset.as_secs() > 0 {
379 write!(f, " (+{})", FormatDuration(offset))?;
380 }
381 }
382 }
383
384 write!(f, ": ")?;
385 fmt_error_with_sources(e.as_ref(), f)?;
386 }
387 Ok(())
388 }
389 }
390 }
391}
392
393struct FormatDuration(Duration);
398
399impl Display for FormatDuration {
400 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
401 fmt_duration_impl(self.0, f)
402 }
403}
404
405struct FormatTimeAgo(Duration);
407
408impl Display for FormatTimeAgo {
409 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
410 let secs = self.0.as_secs();
411 let millis = self.0.as_millis();
412
413 if secs == 0 && millis == 0 {
415 return write!(f, "just now");
416 }
417
418 fmt_duration_impl(self.0, f)?;
419 write!(f, " ago")
420 }
421}
422
423fn fmt_duration_impl(duration: Duration, f: &mut Formatter<'_>) -> fmt::Result {
428 let secs = duration.as_secs();
429
430 if secs == 0 {
431 let millis = duration.as_millis();
432 if millis == 0 {
433 write!(f, "0s")
434 } else {
435 write!(f, "{}ms", millis)
436 }
437 } else if secs < 60 {
438 write!(f, "{}s", secs)
439 } else if secs < 3600 {
440 let mins = secs / 60;
441 let rem_secs = secs % 60;
442 if rem_secs == 0 {
443 write!(f, "{}m", mins)
444 } else {
445 write!(f, "{}m {}s", mins, rem_secs)
446 }
447 } else {
448 let hours = secs / 3600;
449 let mins = (secs % 3600) / 60;
450 if mins == 0 {
451 write!(f, "{}h", hours)
452 } else {
453 write!(f, "{}h {}m", hours, mins)
454 }
455 }
456}
457
458pub fn fmt_error_with_sources(mut e: &dyn Error, f: &mut fmt::Formatter) -> fmt::Result {
499 let mut last = String::new();
504 let mut sep = iter::once("").chain(iter::repeat(": "));
505
506 loop {
510 let this = e.to_string();
511 if !last.contains(&this) {
512 write!(f, "{}{}", sep.next().expect("repeat ended"), &this)?;
513 }
514 last = this;
515
516 if let Some(ne) = e.source() {
517 e = ne;
518 } else {
519 break;
520 }
521 }
522 Ok(())
523}
524
525#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
529fn current_system_time() -> SystemTime {
530 use web_time::web::SystemTimeExt as _;
531 web_time::SystemTime::now().to_std()
532}
533
534#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
538fn current_system_time() -> SystemTime {
539 #![allow(clippy::disallowed_methods)]
540 SystemTime::now()
541}
542
543fn current_instant() -> Instant {
547 #![allow(clippy::disallowed_methods)]
548 Instant::now()
549}
550
551#[cfg(test)]
552mod test {
553 #![allow(clippy::bool_assert_comparison)]
555 #![allow(clippy::clone_on_copy)]
556 #![allow(clippy::dbg_macro)]
557 #![allow(clippy::mixed_attributes_style)]
558 #![allow(clippy::print_stderr)]
559 #![allow(clippy::print_stdout)]
560 #![allow(clippy::single_char_pattern)]
561 #![allow(clippy::unwrap_used)]
562 #![allow(clippy::unchecked_time_subtraction)]
563 #![allow(clippy::useless_vec)]
564 #![allow(clippy::needless_pass_by_value)]
565 #![allow(clippy::disallowed_methods)]
567 use super::*;
568 use derive_more::From;
569
570 #[test]
571 fn bad_parse1() {
572 let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
573 if let Err(e) = "maybe".parse::<bool>() {
574 err.push(e);
575 }
576 if let Err(e) = "a few".parse::<u32>() {
577 err.push(e);
578 }
579 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
580 err.push(e);
581 }
582
583 let disp = format!("{}", err);
584 assert_eq!(
585 disp,
586 "\
587Tried to convert some things 3 times, but all attempts failed
588Attempt 1: provided string was not `true` or `false`
589Attempt 2: invalid digit found in string
590Attempt 3: invalid IP address syntax"
591 );
592
593 let disp_alt = format!("{:#}", err);
594 assert!(disp_alt.contains("Tried to convert some things 3 times, but all attempts failed"));
595 assert!(disp_alt.contains("(from 20")); }
597
598 #[test]
599 fn no_problems() {
600 let empty: RetryError<anyhow::Error> =
601 RetryError::in_attempt_to("immanentize the eschaton");
602 let disp = format!("{}", empty);
603 assert_eq!(
604 disp,
605 "Unable to immanentize the eschaton. (No errors given)"
606 );
607 }
608
609 #[test]
610 fn one_problem() {
611 let mut err: RetryError<anyhow::Error> =
612 RetryError::in_attempt_to("connect to torproject.org");
613 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
614 err.push(e);
615 }
616 let disp = format!("{}", err);
617 assert_eq!(
618 disp,
619 "Unable to connect to torproject.org: invalid IP address syntax"
620 );
621
622 let disp_alt = format!("{:#}", err);
623 assert!(disp_alt.contains("Unable to connect to torproject.org at 20")); assert!(disp_alt.contains("invalid IP address syntax"));
625 }
626
627 #[test]
628 fn operations() {
629 use std::num::ParseIntError;
630
631 #[derive(From, Clone, Debug, Eq, PartialEq)]
632 struct Wrapper(ParseIntError);
633
634 impl AsRef<dyn Error + 'static> for Wrapper {
635 fn as_ref(&self) -> &(dyn Error + 'static) {
636 &self.0
637 }
638 }
639
640 let mut err: RetryError<Wrapper> = RetryError::in_attempt_to("parse some integers");
641 assert!(err.is_empty());
642 assert_eq!(err.len(), 0);
643 err.extend(
644 vec!["not", "your", "number"]
645 .iter()
646 .filter_map(|s| s.parse::<u16>().err())
647 .map(Wrapper),
648 );
649 assert!(!err.is_empty());
650 assert_eq!(err.len(), 3);
651
652 let cloned = err.clone();
653 for (s1, s2) in err.sources().zip(cloned.sources()) {
654 assert_eq!(s1, s2);
655 }
656
657 err.dedup();
658
659 let disp = format!("{}", err);
660 assert_eq!(
661 disp,
662 "\
663Tried to parse some integers 3 times, but all attempts failed
664Attempts 1..3: invalid digit found in string"
665 );
666
667 let disp_alt = format!("{:#}", err);
668 assert!(disp_alt.contains("Tried to parse some integers 3 times, but all attempts failed"));
669 assert!(disp_alt.contains("(from 20")); }
671
672 #[test]
673 fn overflow() {
674 use std::num::ParseIntError;
675 let mut err: RetryError<ParseIntError> =
676 RetryError::in_attempt_to("parse too many integers");
677 assert!(err.is_empty());
678 let mut errors: Vec<ParseIntError> = vec!["no", "numbers"]
679 .iter()
680 .filter_map(|s| s.parse::<u16>().err())
681 .collect();
682 err.n_errors = usize::MAX;
683 err.errors.push((
684 Attempt::Range(1, err.n_errors),
685 errors.pop().expect("parser did not fail"),
686 Instant::now(),
687 ));
688 assert!(err.n_errors == usize::MAX);
689 assert!(err.len() == 1);
690
691 err.push(errors.pop().expect("parser did not fail"));
692 assert!(err.n_errors == usize::MAX);
693 assert!(err.len() == 1);
694 }
695
696 #[test]
697 fn extend_from_retry_preserve_timestamps() {
698 let n1 = Instant::now();
699 let n2 = n1 + Duration::from_secs(10);
700 let n3 = n1 + Duration::from_secs(20);
701
702 let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do first thing");
703 let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do second thing");
704
705 err2.push_timed(anyhow::Error::msg("e1"), n1, None);
706 err2.push_timed(anyhow::Error::msg("e2"), n2, None);
707
708 assert!(err1.first_error_at.is_none());
710
711 err1.extend_from_retry_error(err2);
712
713 assert_eq!(err1.len(), 2);
714 assert_eq!(err1.errors[0].2, n1);
716 assert_eq!(err1.errors[1].2, n2);
717
718 err1.push_timed(anyhow::Error::msg("e3"), n3, None);
720 assert_eq!(err1.len(), 3);
721 assert_eq!(err1.errors[2].2, n3);
722 }
723
724 #[test]
725 fn extend_from_retry_preserve_ranges() {
726 let n1 = Instant::now();
727 let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 1");
728
729 err1.push(anyhow::Error::msg("e1"));
731 err1.push(anyhow::Error::msg("e2"));
732 assert_eq!(err1.n_errors, 2);
733
734 let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 2");
735 err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
737 err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
738 err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
739
740 err2.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
742 assert_eq!(err2.len(), 1); match err2.errors[0].0 {
744 Attempt::Range(1, 3) => {}
745 _ => panic!("Expected range 1..3"),
746 }
747
748 err1.extend_from_retry_error(err2);
750
751 assert_eq!(err1.len(), 3); assert_eq!(err1.n_errors, 5); match err1.errors[2].0 {
756 Attempt::Range(3, 5) => {}
757 ref x => panic!("Expected range 3..5, got {:?}", x),
758 }
759 }
760
761 #[test]
762 fn dedup_after_extend_same_doing() {
763 let doing = "do thing";
764 let message = "error";
765 let n1 = Instant::now();
766 let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to(doing);
767
768 err1.push(anyhow::Error::msg(message));
770 assert_eq!(err1.n_errors, 1);
771
772 let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to(doing);
773 err2.push_timed(anyhow::Error::msg(message), n1, None);
775 err2.push_timed(anyhow::Error::msg(message), n1, None);
776
777 err2.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
779 assert_eq!(err2.len(), 1); match err2.errors[0].0 {
781 Attempt::Range(1, 2) => {}
782 _ => panic!("Expected range 1..2"),
783 }
784
785 err1.extend_from_retry_error(err2);
787 assert_eq!(err1.len(), 2); assert_eq!(err1.n_errors, 3); err1.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
792 assert_eq!(err1.len(), 1); assert_eq!(err1.n_errors, 3); match err1.errors[0].0 {
797 Attempt::Range(1, 3) => {}
798 ref x => panic!("Expected range 1..3, got {:?}", x),
799 }
800 }
801}