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