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