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)] use std::error::Error;
48use std::fmt::{self, Debug, Display, Error as FmtError, Formatter};
49use std::iter;
50use std::time::{Duration, Instant, SystemTime};
51
52#[derive(Debug, Clone)]
64pub struct RetryError<E> {
65 doing: String,
67 errors: Vec<(Attempt, E, Instant)>,
69 n_errors: usize,
74 first_error_at: Option<SystemTime>,
88}
89
90#[derive(Debug, Clone)]
92enum Attempt {
93 Single(usize),
95 Range(usize, usize),
97}
98
99impl<E: Debug + AsRef<dyn Error>> Error for RetryError<E> {}
102
103impl<E> RetryError<E> {
104 pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
115 RetryError {
116 doing: doing.into(),
117 errors: Vec::new(),
118 n_errors: 0,
119 first_error_at: None,
120 }
121 }
122 pub fn push_timed<T>(&mut self, err: T, instant: Instant, wall_clock: Option<SystemTime>)
143 where
144 T: Into<E>,
145 {
146 if self.n_errors < usize::MAX {
147 self.n_errors += 1;
148 let attempt = Attempt::Single(self.n_errors);
149
150 if self.first_error_at.is_none() {
151 self.first_error_at = wall_clock;
152 }
153
154 self.errors.push((attempt, err.into(), instant));
155 }
156 }
157
158 pub fn push<T>(&mut self, err: T)
167 where
168 T: Into<E>,
169 {
170 self.push_timed(err, Instant::now(), Some(SystemTime::now()));
171 }
172
173 pub fn sources(&self) -> impl Iterator<Item = &E> {
176 self.errors.iter().map(|(.., e, _)| e)
177 }
178
179 pub fn len(&self) -> usize {
181 self.errors.len()
182 }
183
184 pub fn is_empty(&self) -> bool {
186 self.errors.is_empty()
187 }
188
189 #[allow(clippy::disallowed_methods)] pub fn extend<T>(&mut self, iter: impl IntoIterator<Item = T>)
204 where
205 T: Into<E>,
206 {
207 for item in iter {
208 self.push(item);
209 }
210 }
211
212 pub fn dedup_by<F>(&mut self, same_err: F)
217 where
218 F: Fn(&E, &E) -> bool,
219 {
220 let mut old_errs = Vec::new();
221 std::mem::swap(&mut old_errs, &mut self.errors);
222
223 for (attempt, err, timestamp) in old_errs {
224 if let Some((last_attempt, last_err, ..)) = self.errors.last_mut() {
225 if same_err(last_err, &err) {
226 last_attempt.grow();
227 } else {
228 self.errors.push((attempt, err, timestamp));
229 }
230 } else {
231 self.errors.push((attempt, err, timestamp));
232 }
233 }
234 }
235
236 pub fn extend_from_retry_error(&mut self, other: RetryError<E>) {
242 if self.first_error_at.is_none() {
243 self.first_error_at = other.first_error_at;
244 }
245
246 for (attempt, err, timestamp) in other.errors {
247 let new_attempt = match attempt {
248 Attempt::Single(_) => {
249 let Some(new_n_errors) = self.n_errors.checked_add(1) else {
250 break;
251 };
252 self.n_errors = new_n_errors;
253 Attempt::Single(new_n_errors)
254 }
255 Attempt::Range(first, last) => {
256 let count = last - first + 1;
257 let Some(new_n_errors) = self.n_errors.checked_add(count) else {
258 break;
259 };
260 let start = self.n_errors + 1;
261 self.n_errors = new_n_errors;
262 Attempt::Range(start, new_n_errors)
263 }
264 };
265
266 self.errors.push((new_attempt, err, timestamp));
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) {
282 *self = match *self {
283 Attempt::Single(idx) => Attempt::Range(idx, idx + 1),
284 Attempt::Range(first, last) => Attempt::Range(first, last + 1),
285 };
286 }
287}
288
289impl<E> IntoIterator for RetryError<E> {
290 type Item = E;
291 type IntoIter = std::vec::IntoIter<E>;
292 #[allow(clippy::needless_collect)]
293 fn into_iter(self) -> Self::IntoIter {
298 self.errors
299 .into_iter()
300 .map(|(.., e, _)| e)
301 .collect::<Vec<_>>()
302 .into_iter()
303 }
304}
305
306impl Display for Attempt {
307 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
308 match self {
309 Attempt::Single(idx) => write!(f, "Attempt {}", idx),
310 Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
311 }
312 }
313}
314
315impl<E: AsRef<dyn Error>> Display for RetryError<E> {
316 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
317 let show_timestamps = f.alternate();
318
319 match self.n_errors {
320 0 => write!(f, "Unable to {}. (No errors given)", self.doing),
321 1 => {
322 write!(f, "Unable to {}", self.doing)?;
323
324 if show_timestamps {
325 if let (Some((.., timestamp)), Some(first_at)) =
326 (self.errors.first(), self.first_error_at)
327 {
328 write!(
329 f,
330 " at {} ({})",
331 humantime::format_rfc3339(first_at),
332 FormatTimeAgo(timestamp.elapsed())
333 )?;
334 }
335 }
336
337 write!(f, ": ")?;
338 fmt_error_with_sources(self.errors[0].1.as_ref(), f)
339 }
340 n => {
341 write!(
342 f,
343 "Tried to {} {} times, but all attempts failed",
344 self.doing, n
345 )?;
346
347 if show_timestamps {
348 if let (Some(first_at), Some((.., first_ts)), Some((.., last_ts))) =
349 (self.first_error_at, self.errors.first(), self.errors.last())
350 {
351 let duration = last_ts.saturating_duration_since(*first_ts);
352
353 write!(f, " (from {} ", humantime::format_rfc3339(first_at))?;
354
355 if duration.as_secs() > 0 {
356 write!(f, "to {}", humantime::format_rfc3339(first_at + duration))?;
357 }
358
359 write!(f, ", {})", FormatTimeAgo(last_ts.elapsed()))?;
360 }
361 }
362
363 let first_ts = self.errors.first().map(|(.., ts)| ts);
364 for (attempt, e, timestamp) in &self.errors {
365 write!(f, "\n{}", attempt)?;
366
367 if show_timestamps {
368 if let Some(first_ts) = first_ts {
369 let offset = timestamp.saturating_duration_since(*first_ts);
370 if offset.as_secs() > 0 {
371 write!(f, " (+{})", FormatDuration(offset))?;
372 }
373 }
374 }
375
376 write!(f, ": ")?;
377 fmt_error_with_sources(e.as_ref(), f)?;
378 }
379 Ok(())
380 }
381 }
382 }
383}
384
385struct FormatDuration(Duration);
390
391impl Display for FormatDuration {
392 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
393 fmt_duration_impl(self.0, f)
394 }
395}
396
397struct FormatTimeAgo(Duration);
399
400impl Display for FormatTimeAgo {
401 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
402 let secs = self.0.as_secs();
403 let millis = self.0.as_millis();
404
405 if secs == 0 && millis == 0 {
407 return write!(f, "just now");
408 }
409
410 fmt_duration_impl(self.0, f)?;
411 write!(f, " ago")
412 }
413}
414
415fn fmt_duration_impl(duration: Duration, f: &mut Formatter<'_>) -> fmt::Result {
420 let secs = duration.as_secs();
421
422 if secs == 0 {
423 let millis = duration.as_millis();
424 if millis == 0 {
425 write!(f, "0s")
426 } else {
427 write!(f, "{}ms", millis)
428 }
429 } else if secs < 60 {
430 write!(f, "{}s", secs)
431 } else if secs < 3600 {
432 let mins = secs / 60;
433 let rem_secs = secs % 60;
434 if rem_secs == 0 {
435 write!(f, "{}m", mins)
436 } else {
437 write!(f, "{}m {}s", mins, rem_secs)
438 }
439 } else {
440 let hours = secs / 3600;
441 let mins = (secs % 3600) / 60;
442 if mins == 0 {
443 write!(f, "{}h", hours)
444 } else {
445 write!(f, "{}h {}m", hours, mins)
446 }
447 }
448}
449
450pub fn fmt_error_with_sources(mut e: &dyn Error, f: &mut fmt::Formatter) -> fmt::Result {
491 let mut last = String::new();
496 let mut sep = iter::once("").chain(iter::repeat(": "));
497 loop {
498 let this = e.to_string();
499 if !last.contains(&this) {
500 write!(f, "{}{}", sep.next().expect("repeat ended"), &this)?;
501 }
502 last = this;
503
504 if let Some(ne) = e.source() {
505 e = ne;
506 } else {
507 break;
508 }
509 }
510 Ok(())
511}
512
513#[cfg(test)]
514mod test {
515 #![allow(clippy::bool_assert_comparison)]
517 #![allow(clippy::clone_on_copy)]
518 #![allow(clippy::dbg_macro)]
519 #![allow(clippy::mixed_attributes_style)]
520 #![allow(clippy::print_stderr)]
521 #![allow(clippy::print_stdout)]
522 #![allow(clippy::single_char_pattern)]
523 #![allow(clippy::unwrap_used)]
524 #![allow(clippy::unchecked_time_subtraction)]
525 #![allow(clippy::useless_vec)]
526 #![allow(clippy::needless_pass_by_value)]
527 #![allow(clippy::disallowed_methods)]
529 use super::*;
530 use derive_more::From;
531
532 #[test]
533 fn bad_parse1() {
534 let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
535 if let Err(e) = "maybe".parse::<bool>() {
536 err.push(e);
537 }
538 if let Err(e) = "a few".parse::<u32>() {
539 err.push(e);
540 }
541 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
542 err.push(e);
543 }
544
545 let disp = format!("{}", err);
546 assert_eq!(
547 disp,
548 "\
549Tried to convert some things 3 times, but all attempts failed
550Attempt 1: provided string was not `true` or `false`
551Attempt 2: invalid digit found in string
552Attempt 3: invalid IP address syntax"
553 );
554
555 let disp_alt = format!("{:#}", err);
556 assert!(disp_alt.contains("Tried to convert some things 3 times, but all attempts failed"));
557 assert!(disp_alt.contains("(from 20")); }
559
560 #[test]
561 fn no_problems() {
562 let empty: RetryError<anyhow::Error> =
563 RetryError::in_attempt_to("immanentize the eschaton");
564 let disp = format!("{}", empty);
565 assert_eq!(
566 disp,
567 "Unable to immanentize the eschaton. (No errors given)"
568 );
569 }
570
571 #[test]
572 fn one_problem() {
573 let mut err: RetryError<anyhow::Error> =
574 RetryError::in_attempt_to("connect to torproject.org");
575 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
576 err.push(e);
577 }
578 let disp = format!("{}", err);
579 assert_eq!(
580 disp,
581 "Unable to connect to torproject.org: invalid IP address syntax"
582 );
583
584 let disp_alt = format!("{:#}", err);
585 assert!(disp_alt.contains("Unable to connect to torproject.org at 20")); assert!(disp_alt.contains("invalid IP address syntax"));
587 }
588
589 #[test]
590 fn operations() {
591 use std::num::ParseIntError;
592
593 #[derive(From, Clone, Debug, Eq, PartialEq)]
594 struct Wrapper(ParseIntError);
595
596 impl AsRef<dyn Error + 'static> for Wrapper {
597 fn as_ref(&self) -> &(dyn Error + 'static) {
598 &self.0
599 }
600 }
601
602 let mut err: RetryError<Wrapper> = RetryError::in_attempt_to("parse some integers");
603 assert!(err.is_empty());
604 assert_eq!(err.len(), 0);
605 err.extend(
606 vec!["not", "your", "number"]
607 .iter()
608 .filter_map(|s| s.parse::<u16>().err())
609 .map(Wrapper),
610 );
611 assert!(!err.is_empty());
612 assert_eq!(err.len(), 3);
613
614 let cloned = err.clone();
615 for (s1, s2) in err.sources().zip(cloned.sources()) {
616 assert_eq!(s1, s2);
617 }
618
619 err.dedup();
620
621 let disp = format!("{}", err);
622 assert_eq!(
623 disp,
624 "\
625Tried to parse some integers 3 times, but all attempts failed
626Attempts 1..3: invalid digit found in string"
627 );
628
629 let disp_alt = format!("{:#}", err);
630 assert!(disp_alt.contains("Tried to parse some integers 3 times, but all attempts failed"));
631 assert!(disp_alt.contains("(from 20")); }
633
634 #[test]
635 fn overflow() {
636 use std::num::ParseIntError;
637 let mut err: RetryError<ParseIntError> =
638 RetryError::in_attempt_to("parse too many integers");
639 assert!(err.is_empty());
640 let mut errors: Vec<ParseIntError> = vec!["no", "numbers"]
641 .iter()
642 .filter_map(|s| s.parse::<u16>().err())
643 .collect();
644 err.n_errors = usize::MAX;
645 err.errors.push((
646 Attempt::Range(1, err.n_errors),
647 errors.pop().expect("parser did not fail"),
648 Instant::now(),
649 ));
650 assert!(err.n_errors == usize::MAX);
651 assert!(err.len() == 1);
652
653 err.push(errors.pop().expect("parser did not fail"));
654 assert!(err.n_errors == usize::MAX);
655 assert!(err.len() == 1);
656 }
657
658 #[test]
659 fn extend_from_retry_preserve_timestamps() {
660 let n1 = Instant::now();
661 let n2 = n1 + Duration::from_secs(10);
662 let n3 = n1 + Duration::from_secs(20);
663
664 let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do first thing");
665 let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do second thing");
666
667 err2.push_timed(anyhow::Error::msg("e1"), n1, None);
668 err2.push_timed(anyhow::Error::msg("e2"), n2, None);
669
670 assert!(err1.first_error_at.is_none());
672
673 err1.extend_from_retry_error(err2);
674
675 assert_eq!(err1.len(), 2);
676 assert_eq!(err1.errors[0].2, n1);
678 assert_eq!(err1.errors[1].2, n2);
679
680 err1.push_timed(anyhow::Error::msg("e3"), n3, None);
682 assert_eq!(err1.len(), 3);
683 assert_eq!(err1.errors[2].2, n3);
684 }
685
686 #[test]
687 fn extend_from_retry_preserve_ranges() {
688 let n1 = Instant::now();
689 let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 1");
690
691 err1.push(anyhow::Error::msg("e1"));
693 err1.push(anyhow::Error::msg("e2"));
694 assert_eq!(err1.n_errors, 2);
695
696 let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 2");
697 let _err_msg = anyhow::Error::msg("repeated");
699 err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
700 err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
701 err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
702
703 err2.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
705 assert_eq!(err2.len(), 1); match err2.errors[0].0 {
707 Attempt::Range(1, 3) => {}
708 _ => panic!("Expected range 1..3"),
709 }
710
711 err1.extend_from_retry_error(err2);
713
714 assert_eq!(err1.len(), 3); assert_eq!(err1.n_errors, 5); match err1.errors[2].0 {
719 Attempt::Range(3, 5) => {}
720 ref x => panic!("Expected range 3..5, got {:?}", x),
721 }
722 }
723}