retry_error/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2#![doc = include_str!("../README.md")]
3// @@ begin lint list maintained by maint/add_warning @@
4#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6#![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_duration_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)] // This can reasonably be done for explicitness
39#![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43#![allow(clippy::needless_lifetimes)] // See arti#1765
44//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
45
46use std::error::Error;
47use std::fmt::{self, Debug, Display, Error as FmtError, Formatter};
48use std::iter;
49
50/// An error type for use when we're going to do something a few times,
51/// and they might all fail.
52///
53/// To use this error type, initialize a new RetryError before you
54/// start trying to do whatever it is.  Then, every time the operation
55/// fails, use [`RetryError::push()`] to add a new error to the list
56/// of errors.  If the operation fails too many times, you can use
57/// RetryError as an [`Error`] itself.
58#[derive(Debug, Clone)]
59pub struct RetryError<E> {
60    /// The operation we were trying to do.
61    doing: String,
62    /// The errors that we encountered when doing the operation.
63    errors: Vec<(Attempt, E)>,
64    /// The total number of errors we encountered.
65    ///
66    /// This can differ from errors.len() if the errors have been
67    /// deduplicated.
68    n_errors: usize,
69}
70
71/// Represents which attempts, in sequence, failed to complete.
72#[derive(Debug, Clone)]
73enum Attempt {
74    /// A single attempt that failed.
75    Single(usize),
76    /// A range of consecutive attempts that failed.
77    Range(usize, usize),
78}
79
80// TODO: Should we declare that some error is the 'source' of this one?
81// If so, should it be the first failure?  The last?
82impl<E: Debug + AsRef<dyn Error>> Error for RetryError<E> {}
83
84impl<E> RetryError<E> {
85    /// Create a new RetryError, with no failed attempts.
86    ///
87    /// The provided `doing` argument is a short string that describes
88    /// what we were trying to do when we failed too many times.  It
89    /// will be used to format the final error message; it should be a
90    /// phrase that can go after "while trying to".
91    ///
92    /// This RetryError should not be used as-is, since when no
93    /// [`Error`]s have been pushed into it, it doesn't represent an
94    /// actual failure.
95    pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
96        RetryError {
97            doing: doing.into(),
98            errors: Vec::new(),
99            n_errors: 0,
100        }
101    }
102    /// Add an error to this RetryError.
103    ///
104    /// You should call this method when an attempt at the underlying operation
105    /// has failed.
106    pub fn push<T>(&mut self, err: T)
107    where
108        T: Into<E>,
109    {
110        if self.n_errors < usize::MAX {
111            self.n_errors += 1;
112            let attempt = Attempt::Single(self.n_errors);
113            self.errors.push((attempt, err.into()));
114        }
115    }
116
117    /// Return an iterator over all of the reasons that the attempt
118    /// behind this RetryError has failed.
119    pub fn sources(&self) -> impl Iterator<Item = &E> {
120        self.errors.iter().map(|(_, e)| e)
121    }
122
123    /// Return the number of underlying errors.
124    pub fn len(&self) -> usize {
125        self.errors.len()
126    }
127
128    /// Return true if no underlying errors have been added.
129    pub fn is_empty(&self) -> bool {
130        self.errors.is_empty()
131    }
132
133    /// Group up consecutive errors of the same kind, for easier display.
134    ///
135    /// Two errors have "the same kind" if they return `true` when passed
136    /// to the provided `dedup` function.
137    pub fn dedup_by<F>(&mut self, same_err: F)
138    where
139        F: Fn(&E, &E) -> bool,
140    {
141        let mut old_errs = Vec::new();
142        std::mem::swap(&mut old_errs, &mut self.errors);
143
144        for (attempt, err) in old_errs {
145            if let Some((ref mut last_attempt, last_err)) = self.errors.last_mut() {
146                if same_err(last_err, &err) {
147                    last_attempt.grow();
148                } else {
149                    self.errors.push((attempt, err));
150                }
151            } else {
152                self.errors.push((attempt, err));
153            }
154        }
155    }
156}
157
158impl<E: PartialEq<E>> RetryError<E> {
159    /// Group up consecutive errors of the same kind, according to the
160    /// `PartialEq` implementation.
161    pub fn dedup(&mut self) {
162        self.dedup_by(PartialEq::eq);
163    }
164}
165
166impl Attempt {
167    /// Extend this attempt by a single additional failure.
168    fn grow(&mut self) {
169        *self = match *self {
170            Attempt::Single(idx) => Attempt::Range(idx, idx + 1),
171            Attempt::Range(first, last) => Attempt::Range(first, last + 1),
172        };
173    }
174}
175
176impl<E, T> Extend<T> for RetryError<E>
177where
178    T: Into<E>,
179{
180    fn extend<C>(&mut self, iter: C)
181    where
182        C: IntoIterator<Item = T>,
183    {
184        for item in iter.into_iter() {
185            self.push(item);
186        }
187    }
188}
189
190impl<E> IntoIterator for RetryError<E> {
191    type Item = E;
192    type IntoIter = std::vec::IntoIter<E>;
193    #[allow(clippy::needless_collect)]
194    // TODO We have to use collect/into_iter here for now, since
195    // the actual Map<> type can't be named.  Once Rust lets us say
196    // `type IntoIter = impl Iterator<Item=E>` then we fix the code
197    // and turn the Clippy warning back on.
198    fn into_iter(self) -> Self::IntoIter {
199        let v: Vec<_> = self.errors.into_iter().map(|x| x.1).collect();
200        v.into_iter()
201    }
202}
203
204impl Display for Attempt {
205    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
206        match self {
207            Attempt::Single(idx) => write!(f, "Attempt {}", idx),
208            Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
209        }
210    }
211}
212
213impl<E: AsRef<dyn Error>> Display for RetryError<E> {
214    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
215        match self.n_errors {
216            0 => write!(f, "Unable to {}. (No errors given)", self.doing),
217            1 => {
218                write!(f, "Unable to {}: ", self.doing)?;
219                fmt_error_with_sources(self.errors[0].1.as_ref(), f)
220            }
221            n => {
222                write!(
223                    f,
224                    "Tried to {} {} times, but all attempts failed",
225                    self.doing, n
226                )?;
227
228                for (attempt, e) in &self.errors {
229                    write!(f, "\n{}: ", attempt)?;
230                    fmt_error_with_sources(e.as_ref(), f)?;
231                }
232                Ok(())
233            }
234        }
235    }
236}
237
238/// Helper: formats a [`std::error::Error`] and its sources (as `"error: source"`)
239///
240/// Avoids duplication in messages by not printing messages which are
241/// wholly-contained (textually) within already-printed messages.
242///
243/// Offered as a `fmt` function:
244/// this is for use in more-convenient higher-level error handling functionality,
245/// rather than directly in application/functional code.
246///
247/// This is used by `RetryError`'s impl of `Display`,
248/// but will be useful for other error-handling situations.
249///
250/// # Example
251///
252/// ```
253/// use std::fmt::{self, Display};
254///
255/// #[derive(Debug, thiserror::Error)]
256/// #[error("some pernickety problem")]
257/// struct Pernickety;
258///
259/// #[derive(Debug, thiserror::Error)]
260/// enum ApplicationError {
261///     #[error("everything is terrible")]
262///     Terrible(#[source] Pernickety),
263/// }
264///
265/// struct Wrapper(Box<dyn std::error::Error>);
266/// impl Display for Wrapper {
267///     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
268///         retry_error::fmt_error_with_sources(&*self.0, f)
269///     }
270/// }
271///
272/// let bad = Pernickety;
273/// let err = ApplicationError::Terrible(bad);
274///
275/// let printed = Wrapper(err.into()).to_string();
276/// assert_eq!(printed, "everything is terrible: some pernickety problem");
277/// ```
278pub fn fmt_error_with_sources(mut e: &dyn Error, f: &mut fmt::Formatter) -> fmt::Result {
279    let mut last = String::new();
280    let mut sep = iter::once("").chain(iter::repeat(": "));
281    loop {
282        let this = e.to_string();
283        if !last.contains(&this) {
284            write!(f, "{}{}", sep.next().expect("repeat ended"), &this)?;
285        }
286        last = this;
287
288        if let Some(ne) = e.source() {
289            e = ne;
290        } else {
291            break;
292        }
293    }
294    Ok(())
295}
296
297#[cfg(test)]
298mod test {
299    // @@ begin test lint list maintained by maint/add_warning @@
300    #![allow(clippy::bool_assert_comparison)]
301    #![allow(clippy::clone_on_copy)]
302    #![allow(clippy::dbg_macro)]
303    #![allow(clippy::mixed_attributes_style)]
304    #![allow(clippy::print_stderr)]
305    #![allow(clippy::print_stdout)]
306    #![allow(clippy::single_char_pattern)]
307    #![allow(clippy::unwrap_used)]
308    #![allow(clippy::unchecked_duration_subtraction)]
309    #![allow(clippy::useless_vec)]
310    #![allow(clippy::needless_pass_by_value)]
311    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
312    use super::*;
313    use derive_more::From;
314
315    #[test]
316    fn bad_parse1() {
317        let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
318        if let Err(e) = "maybe".parse::<bool>() {
319            err.push(e);
320        }
321        if let Err(e) = "a few".parse::<u32>() {
322            err.push(e);
323        }
324        if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
325            err.push(e);
326        }
327        let disp = format!("{}", err);
328        assert_eq!(
329            disp,
330            "\
331Tried to convert some things 3 times, but all attempts failed
332Attempt 1: provided string was not `true` or `false`
333Attempt 2: invalid digit found in string
334Attempt 3: invalid IP address syntax"
335        );
336    }
337
338    #[test]
339    fn no_problems() {
340        let empty: RetryError<anyhow::Error> =
341            RetryError::in_attempt_to("immanentize the eschaton");
342        let disp = format!("{}", empty);
343        assert_eq!(
344            disp,
345            "Unable to immanentize the eschaton. (No errors given)"
346        );
347    }
348
349    #[test]
350    fn one_problem() {
351        let mut err: RetryError<anyhow::Error> =
352            RetryError::in_attempt_to("connect to torproject.org");
353        if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
354            err.push(e);
355        }
356        let disp = format!("{}", err);
357        assert_eq!(
358            disp,
359            "Unable to connect to torproject.org: invalid IP address syntax"
360        );
361    }
362
363    #[test]
364    fn operations() {
365        use std::num::ParseIntError;
366
367        #[derive(From, Clone, Debug, Eq, PartialEq)]
368        struct Wrapper(ParseIntError);
369
370        impl AsRef<dyn Error + 'static> for Wrapper {
371            fn as_ref(&self) -> &(dyn Error + 'static) {
372                &self.0
373            }
374        }
375
376        let mut err: RetryError<Wrapper> = RetryError::in_attempt_to("parse some integers");
377        assert!(err.is_empty());
378        assert_eq!(err.len(), 0);
379        err.extend(
380            vec!["not", "your", "number"]
381                .iter()
382                .filter_map(|s| s.parse::<u16>().err())
383                .map(Wrapper),
384        );
385        assert!(!err.is_empty());
386        assert_eq!(err.len(), 3);
387
388        let cloned = err.clone();
389        for (s1, s2) in err.sources().zip(cloned.sources()) {
390            assert_eq!(s1, s2);
391        }
392
393        err.dedup();
394        let disp = format!("{}", err);
395        assert_eq!(
396            disp,
397            "\
398Tried to parse some integers 3 times, but all attempts failed
399Attempts 1..3: invalid digit found in string"
400        );
401    }
402
403    #[test]
404    fn overflow() {
405        use std::num::ParseIntError;
406        let mut err: RetryError<ParseIntError> =
407            RetryError::in_attempt_to("parse too many integers");
408        assert!(err.is_empty());
409        let mut errors: Vec<ParseIntError> = vec!["no", "numbers"]
410            .iter()
411            .filter_map(|s| s.parse::<u16>().err())
412            .collect();
413        err.n_errors = usize::MAX;
414        err.errors.push((
415            Attempt::Range(1, err.n_errors),
416            errors.pop().expect("parser did not fail"),
417        ));
418        assert!(err.n_errors == usize::MAX);
419        assert!(err.len() == 1);
420
421        err.push(errors.pop().expect("parser did not fail"));
422        assert!(err.n_errors == usize::MAX);
423        assert!(err.len() == 1);
424    }
425}