1 //! An error attempt to represent multiple failures.
2 //!
3 //! This crate implements [`RetryError`], a type to use when you
4 //! retry something a few times, and all those attempts.  Instead of
5 //! returning only a single error, it records _all of the errors
6 //! received_, in case they are different.
7 //!
8 //! This crate is developed as part of
9 //! [Arti](https://gitlab.torproject.org/tpo/core/arti/), a project to
10 //! implement [Tor](https://www.torproject.org/) in Rust.
11 //! It's used by higher-level crates that retry
12 //! operations.
13 //!
14 //! ## Example
15 //!
16 //! ```rust
17 //!use retry_error::RetryError;
18 //!
19 //!fn some_operation() -> anyhow::Result<bool> {
20 //!    unimplemented!(); // example
21 //!}
22 //!
23 //!fn example() -> Result<(), RetryError<anyhow::Error>> {
24 //!    const N_ATTEMPTS: usize = 10;
25 //!    let mut err = RetryError::in_attempt_to("perform an example operation");
26 //!    for _ in 0..N_ATTEMPTS {
27 //!        match some_operation() {
28 //!            Ok(val) => return Ok(()),
29 //!            Err(e) => err.push(e),
30 //!        }
31 //!    }
32 //!    // All attempts failed; return all the errors.
33 //!    return Err(err);
34 //!}
35 //! ```
36 
37 #![deny(missing_docs)]
38 #![warn(noop_method_call)]
39 #![deny(unreachable_pub)]
40 #![deny(clippy::await_holding_lock)]
41 #![deny(clippy::cargo_common_metadata)]
42 #![deny(clippy::cast_lossless)]
43 #![deny(clippy::checked_conversions)]
44 #![warn(clippy::clone_on_ref_ptr)]
45 #![warn(clippy::cognitive_complexity)]
46 #![deny(clippy::debug_assert_with_mut_call)]
47 #![deny(clippy::exhaustive_enums)]
48 #![deny(clippy::exhaustive_structs)]
49 #![deny(clippy::expl_impl_clone_on_copy)]
50 #![deny(clippy::fallible_impl_from)]
51 #![deny(clippy::implicit_clone)]
52 #![deny(clippy::large_stack_arrays)]
53 #![warn(clippy::manual_ok_or)]
54 #![deny(clippy::missing_docs_in_private_items)]
55 #![deny(clippy::missing_panics_doc)]
56 #![warn(clippy::needless_borrow)]
57 #![warn(clippy::needless_pass_by_value)]
58 #![warn(clippy::option_option)]
59 #![warn(clippy::rc_buffer)]
60 #![deny(clippy::ref_option_ref)]
61 #![warn(clippy::semicolon_if_nothing_returned)]
62 #![warn(clippy::trait_duplication_in_bounds)]
63 #![deny(clippy::unnecessary_wraps)]
64 #![warn(clippy::unseparated_literal_suffix)]
65 #![deny(clippy::unwrap_used)]
66 
67 use std::error::Error;
68 use std::fmt::{Debug, Display, Error as FmtError, Formatter};
69 
70 /// An error type for use when we're going to do something a few times,
71 /// and they might all fail.
72 ///
73 /// To use this error type, initialize a new RetryError before you
74 /// start trying to do whatever it is.  Then, every time the operation
75 /// fails, use [`RetryError::push()`] to add a new error to the list
76 /// of errors.  If the operation fails too many times, you can use
77 /// RetryError as an [`Error`] itself.
78 #[derive(Debug)]
79 pub struct RetryError<E> {
80     /// The operation we were trying to do.
81     doing: String,
82     /// The errors that we encountered when doing the operation.
83     errors: Vec<(Attempt, E)>,
84     /// The total number of errors we encountered.
85     ///
86     /// This can differ from errors.len() if the errors have been
87     /// deduplicated.
88     n_errors: usize,
89 }
90 
91 /// Represents which attempts, in sequence, failed to complete.
92 #[derive(Debug, Clone)]
93 enum Attempt {
94     /// A single attempt that failed.
95     Single(usize),
96     /// A range of consecutive attempts that failed.
97     Range(usize, usize),
98 }
99 
100 // TODO: Should we declare that some error is the 'source' of this one?
101 // If so, should it be the first failure?  The last?
102 impl<E: Debug + Display> Error for RetryError<E> {}
103 
104 impl<E> RetryError<E> {
105     /// Crate a new RetryError, with no failed attempts,
106     ///
107     /// The provided `doing` argument is a short string that describes
108     /// what we were trying to do when we failed too many times.  It
109     /// will be used to format the final error message; it should be a
110     /// phrase that can go after "while trying to".
111     ///
112     /// This RetryError should not be used as-is, since when no
113     /// [`Error`]s have been pushed into it, it doesn't represent an
114     /// actual failure.
in_attempt_to<T: Into<String>>(doing: T) -> Self115     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         }
121     }
122     /// Add an error to this RetryError.
123     ///
124     /// You should call this method when an attempt at the underlying operation
125     /// has failed.
push<T>(&mut self, err: T) where T: Into<E>,126     pub fn push<T>(&mut self, err: T)
127     where
128         T: Into<E>,
129     {
130         self.n_errors += 1;
131         let attempt = Attempt::Single(self.n_errors);
132         self.errors.push((attempt, err.into()));
133     }
134 
135     /// Return an iterator over all of the reasons that the attempt
136     /// behind this RetryError has failed.
sources(&self) -> impl Iterator<Item = &E>137     pub fn sources(&self) -> impl Iterator<Item = &E> {
138         self.errors.iter().map(|(_, e)| e)
139     }
140 
141     /// Return the number of underlying errors.
len(&self) -> usize142     pub fn len(&self) -> usize {
143         self.errors.len()
144     }
145 
146     /// Return true if no underlying errors have been added.
is_empty(&self) -> bool147     pub fn is_empty(&self) -> bool {
148         self.errors.is_empty()
149     }
150 
151     /// Group up consecutive errors of the same kind, for easier display.
152     ///
153     /// Two errors have "the same kind" if they return `true` when passed
154     /// to the provided `dedup` function.
dedup_by<F>(&mut self, same_err: F) where F: Fn(&E, &E) -> bool,155     pub fn dedup_by<F>(&mut self, same_err: F)
156     where
157         F: Fn(&E, &E) -> bool,
158     {
159         let mut old_errs = Vec::new();
160         std::mem::swap(&mut old_errs, &mut self.errors);
161 
162         for (attempt, err) in old_errs {
163             if let Some((ref mut last_attempt, last_err)) = self.errors.last_mut() {
164                 if same_err(last_err, &err) {
165                     last_attempt.grow();
166                 } else {
167                     self.errors.push((attempt, err));
168                 }
169             } else {
170                 self.errors.push((attempt, err));
171             }
172         }
173     }
174 }
175 
176 impl<E: PartialEq<E>> RetryError<E> {
177     /// Group up consecutive errors of the same kind, according to the
178     /// `PartialEq` implementation.
dedup(&mut self)179     pub fn dedup(&mut self) {
180         self.dedup_by(PartialEq::eq);
181     }
182 }
183 
184 impl Attempt {
185     /// Extend this attempt by a single additional failure.
grow(&mut self)186     fn grow(&mut self) {
187         *self = match *self {
188             Attempt::Single(idx) => Attempt::Range(idx, idx + 1),
189             Attempt::Range(first, last) => Attempt::Range(first, last + 1),
190         };
191     }
192 }
193 
194 impl<E: Clone> Clone for RetryError<E> {
clone(&self) -> RetryError<E>195     fn clone(&self) -> RetryError<E> {
196         RetryError {
197             doing: self.doing.clone(),
198             errors: self.errors.clone(),
199             n_errors: self.n_errors,
200         }
201     }
202 }
203 
204 impl<E, T> Extend<T> for RetryError<E>
205 where
206     T: Into<E>,
207 {
extend<C>(&mut self, iter: C) where C: IntoIterator<Item = T>,208     fn extend<C>(&mut self, iter: C)
209     where
210         C: IntoIterator<Item = T>,
211     {
212         for item in iter.into_iter() {
213             self.push(item);
214         }
215     }
216 }
217 
218 impl<E> IntoIterator for RetryError<E> {
219     type Item = E;
220     type IntoIter = std::vec::IntoIter<E>;
221     #[allow(clippy::needless_collect)]
222     // TODO We have to use collect/into_iter here for now, since
223     // the actual Map<> type can't be named.  Once Rust lets us say
224     // `type IntoIter = impl Iterator<Item=E>` then we fix the code
225     // and turn the Clippy warning back on.
into_iter(self) -> Self::IntoIter226     fn into_iter(self) -> Self::IntoIter {
227         let v: Vec<_> = self.errors.into_iter().map(|x| x.1).collect();
228         v.into_iter()
229     }
230 }
231 
232 impl Display for Attempt {
fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError>233     fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
234         match self {
235             Attempt::Single(idx) => write!(f, "Attempt {}", idx),
236             Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
237         }
238     }
239 }
240 
241 impl<E: Display> Display for RetryError<E> {
fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError>242     fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
243         match self.n_errors {
244             0 => write!(f, "Unable to {}. (No errors given)", self.doing),
245             1 => write!(f, "Unable to {}: {}", self.doing, self.errors[0].1),
246             n => {
247                 write!(
248                     f,
249                     "Tried to {} {} times, but all attempts failed.",
250                     self.doing, n
251                 )?;
252 
253                 for (attempt, e) in &self.errors {
254                     write!(f, "\n{}: {}", attempt, e)?;
255                 }
256                 Ok(())
257             }
258         }
259     }
260 }
261 
262 #[cfg(test)]
263 mod test {
264     use super::*;
265 
266     #[test]
bad_parse1()267     fn bad_parse1() {
268         let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
269         if let Err(e) = "maybe".parse::<bool>() {
270             err.push(e);
271         }
272         if let Err(e) = "a few".parse::<u32>() {
273             err.push(e);
274         }
275         if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
276             err.push(e);
277         }
278         let disp = format!("{}", err);
279         assert_eq!(
280             disp,
281             "\
282 Tried to convert some things 3 times, but all attempts failed.
283 Attempt 1: provided string was not `true` or `false`
284 Attempt 2: invalid digit found in string
285 Attempt 3: invalid IP address syntax"
286         );
287     }
288 
289     #[test]
no_problems()290     fn no_problems() {
291         let empty: RetryError<anyhow::Error> =
292             RetryError::in_attempt_to("immanentize the eschaton");
293         let disp = format!("{}", empty);
294         assert_eq!(
295             disp,
296             "Unable to immanentize the eschaton. (No errors given)"
297         );
298     }
299 
300     #[test]
one_problem()301     fn one_problem() {
302         let mut err: RetryError<anyhow::Error> =
303             RetryError::in_attempt_to("connect to torproject.org");
304         if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
305             err.push(e);
306         }
307         let disp = format!("{}", err);
308         assert_eq!(
309             disp,
310             "Unable to connect to torproject.org: invalid IP address syntax"
311         );
312     }
313 
314     #[test]
operations()315     fn operations() {
316         use std::num::ParseIntError;
317         let mut err: RetryError<ParseIntError> = RetryError::in_attempt_to("parse some integers");
318         assert!(err.is_empty());
319         assert_eq!(err.len(), 0);
320         err.extend(
321             vec!["not", "your", "number"]
322                 .iter()
323                 .filter_map(|s| s.parse::<u16>().err()),
324         );
325         assert!(!err.is_empty());
326         assert_eq!(err.len(), 3);
327 
328         let cloned = err.clone();
329         for (s1, s2) in err.sources().zip(cloned.sources()) {
330             assert_eq!(s1, s2);
331         }
332 
333         err.dedup();
334         let disp = format!("{}", err);
335         assert_eq!(
336             disp,
337             "\
338 Tried to parse some integers 3 times, but all attempts failed.
339 Attempts 1..3: invalid digit found in string"
340         );
341     }
342 }
343