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