1
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
#![warn(clippy::all)]
41
#![deny(clippy::await_holding_lock)]
42
#![deny(clippy::cargo_common_metadata)]
43
#![deny(clippy::cast_lossless)]
44
#![deny(clippy::checked_conversions)]
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
1
#[derive(Debug, Clone)]
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
3
#[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.
115
98
    pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
116
98
        RetryError {
117
98
            doing: doing.into(),
118
98
            errors: Vec::new(),
119
98
            n_errors: 0,
120
98
        }
121
98
    }
122
    /// Add an error to this RetryError.
123
    ///
124
    /// You should call this method when an attempt at the underlying operation
125
    /// has failed.
126
117
    pub fn push<T>(&mut self, err: T)
127
117
    where
128
117
        T: Into<E>,
129
117
    {
130
117
        self.n_errors += 1;
131
117
        let attempt = Attempt::Single(self.n_errors);
132
117
        self.errors.push((attempt, err.into()));
133
117
    }
134

            
135
    /// Return an iterator over all of the reasons that the attempt
136
    /// behind this RetryError has failed.
137
2
    pub fn sources(&self) -> impl Iterator<Item = &E> {
138
6
        self.errors.iter().map(|(_, e)| e)
139
2
    }
140

            
141
    /// Return the number of underlying errors.
142
2
    pub fn len(&self) -> usize {
143
2
        self.errors.len()
144
2
    }
145

            
146
    /// Return true if no underlying errors have been added.
147
2
    pub fn is_empty(&self) -> bool {
148
2
        self.errors.is_empty()
149
2
    }
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.
155
1
    pub fn dedup_by<F>(&mut self, same_err: F)
156
1
    where
157
1
        F: Fn(&E, &E) -> bool,
158
1
    {
159
1
        let mut old_errs = Vec::new();
160
1
        std::mem::swap(&mut old_errs, &mut self.errors);
161

            
162
4
        for (attempt, err) in old_errs {
163
3
            if let Some((ref mut last_attempt, last_err)) = self.errors.last_mut() {
164
2
                if same_err(last_err, &err) {
165
2
                    last_attempt.grow();
166
2
                } else {
167
                    self.errors.push((attempt, err));
168
                }
169
1
            } else {
170
1
                self.errors.push((attempt, err));
171
1
            }
172
        }
173
1
    }
174
}
175

            
176
impl<E: PartialEq<E>> RetryError<E> {
177
    /// Group up consecutive errors of the same kind, according to the
178
    /// `PartialEq` implementation.
179
1
    pub fn dedup(&mut self) {
180
1
        self.dedup_by(PartialEq::eq);
181
1
    }
182
}
183

            
184
impl Attempt {
185
    /// Extend this attempt by a single additional failure.
186
2
    fn grow(&mut self) {
187
2
        *self = match *self {
188
1
            Attempt::Single(idx) => Attempt::Range(idx, idx + 1),
189
1
            Attempt::Range(first, last) => Attempt::Range(first, last + 1),
190
        };
191
2
    }
192
}
193

            
194
impl<E, T> Extend<T> for RetryError<E>
195
where
196
    T: Into<E>,
197
{
198
39
    fn extend<C>(&mut self, iter: C)
199
39
    where
200
39
        C: IntoIterator<Item = T>,
201
39
    {
202
41
        for item in iter.into_iter() {
203
41
            self.push(item);
204
41
        }
205
39
    }
206
}
207

            
208
impl<E> IntoIterator for RetryError<E> {
209
    type Item = E;
210
    type IntoIter = std::vec::IntoIter<E>;
211
    #[allow(clippy::needless_collect)]
212
    // TODO We have to use collect/into_iter here for now, since
213
    // the actual Map<> type can't be named.  Once Rust lets us say
214
    // `type IntoIter = impl Iterator<Item=E>` then we fix the code
215
    // and turn the Clippy warning back on.
216
38
    fn into_iter(self) -> Self::IntoIter {
217
38
        let v: Vec<_> = self.errors.into_iter().map(|x| x.1).collect();
218
38
        v.into_iter()
219
38
    }
220
}
221

            
222
impl Display for Attempt {
223
4
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
224
4
        match self {
225
3
            Attempt::Single(idx) => write!(f, "Attempt {}", idx),
226
1
            Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
227
        }
228
4
    }
229
}
230

            
231
impl<E: Display> Display for RetryError<E> {
232
4
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
233
4
        match self.n_errors {
234
1
            0 => write!(f, "Unable to {}. (No errors given)", self.doing),
235
1
            1 => write!(f, "Unable to {}: {}", self.doing, self.errors[0].1),
236
2
            n => {
237
2
                write!(
238
2
                    f,
239
2
                    "Tried to {} {} times, but all attempts failed.",
240
2
                    self.doing, n
241
2
                )?;
242

            
243
6
                for (attempt, e) in &self.errors {
244
4
                    write!(f, "\n{}: {}", attempt, e)?;
245
                }
246
2
                Ok(())
247
            }
248
        }
249
4
    }
250
}
251

            
252
#[cfg(test)]
253
mod test {
254
    use super::*;
255

            
256
    #[test]
257
    fn bad_parse1() {
258
        let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
259
        if let Err(e) = "maybe".parse::<bool>() {
260
            err.push(e);
261
        }
262
        if let Err(e) = "a few".parse::<u32>() {
263
            err.push(e);
264
        }
265
        if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
266
            err.push(e);
267
        }
268
        let disp = format!("{}", err);
269
        assert_eq!(
270
            disp,
271
            "\
272
Tried to convert some things 3 times, but all attempts failed.
273
Attempt 1: provided string was not `true` or `false`
274
Attempt 2: invalid digit found in string
275
Attempt 3: invalid IP address syntax"
276
        );
277
    }
278

            
279
    #[test]
280
    fn no_problems() {
281
        let empty: RetryError<anyhow::Error> =
282
            RetryError::in_attempt_to("immanentize the eschaton");
283
        let disp = format!("{}", empty);
284
        assert_eq!(
285
            disp,
286
            "Unable to immanentize the eschaton. (No errors given)"
287
        );
288
    }
289

            
290
    #[test]
291
    fn one_problem() {
292
        let mut err: RetryError<anyhow::Error> =
293
            RetryError::in_attempt_to("connect to torproject.org");
294
        if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
295
            err.push(e);
296
        }
297
        let disp = format!("{}", err);
298
        assert_eq!(
299
            disp,
300
            "Unable to connect to torproject.org: invalid IP address syntax"
301
        );
302
    }
303

            
304
    #[test]
305
    fn operations() {
306
        use std::num::ParseIntError;
307
        let mut err: RetryError<ParseIntError> = RetryError::in_attempt_to("parse some integers");
308
        assert!(err.is_empty());
309
        assert_eq!(err.len(), 0);
310
        err.extend(
311
            vec!["not", "your", "number"]
312
                .iter()
313
                .filter_map(|s| s.parse::<u16>().err()),
314
        );
315
        assert!(!err.is_empty());
316
        assert_eq!(err.len(), 3);
317

            
318
        let cloned = err.clone();
319
        for (s1, s2) in err.sources().zip(cloned.sources()) {
320
            assert_eq!(s1, s2);
321
        }
322

            
323
        err.dedup();
324
        let disp = format!("{}", err);
325
        assert_eq!(
326
            disp,
327
            "\
328
Tried to parse some integers 3 times, but all attempts failed.
329
Attempts 1..3: invalid digit found in string"
330
        );
331
    }
332
}