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

            
46
use std::error::Error;
47
use std::fmt::{self, Debug, Display, Error as FmtError, Formatter};
48
use 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)]
59
pub 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)]
73
enum 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?
82
impl<E: Debug + AsRef<dyn Error>> Error for RetryError<E> {}
83

            
84
impl<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
780
    pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
96
780
        RetryError {
97
780
            doing: doing.into(),
98
780
            errors: Vec::new(),
99
780
            n_errors: 0,
100
780
        }
101
780
    }
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
290
    pub fn push<T>(&mut self, err: T)
107
290
    where
108
290
        T: Into<E>,
109
290
    {
110
290
        if self.n_errors < usize::MAX {
111
288
            self.n_errors += 1;
112
288
            let attempt = Attempt::Single(self.n_errors);
113
288
            self.errors.push((attempt, err.into()));
114
288
        }
115
290
    }
116

            
117
    /// Return an iterator over all of the reasons that the attempt
118
    /// behind this RetryError has failed.
119
84
    pub fn sources(&self) -> impl Iterator<Item = &E> {
120
92
        self.errors.iter().map(|(_, e)| e)
121
84
    }
122

            
123
    /// Return the number of underlying errors.
124
8
    pub fn len(&self) -> usize {
125
8
        self.errors.len()
126
8
    }
127

            
128
    /// Return true if no underlying errors have been added.
129
6
    pub fn is_empty(&self) -> bool {
130
6
        self.errors.is_empty()
131
6
    }
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
2
    pub fn dedup_by<F>(&mut self, same_err: F)
138
2
    where
139
2
        F: Fn(&E, &E) -> bool,
140
2
    {
141
2
        let mut old_errs = Vec::new();
142
2
        std::mem::swap(&mut old_errs, &mut self.errors);
143

            
144
8
        for (attempt, err) in old_errs {
145
6
            if let Some((ref mut last_attempt, last_err)) = self.errors.last_mut() {
146
4
                if same_err(last_err, &err) {
147
4
                    last_attempt.grow();
148
4
                } else {
149
                    self.errors.push((attempt, err));
150
                }
151
2
            } else {
152
2
                self.errors.push((attempt, err));
153
2
            }
154
        }
155
2
    }
156
}
157

            
158
impl<E: PartialEq<E>> RetryError<E> {
159
    /// Group up consecutive errors of the same kind, according to the
160
    /// `PartialEq` implementation.
161
2
    pub fn dedup(&mut self) {
162
2
        self.dedup_by(PartialEq::eq);
163
2
    }
164
}
165

            
166
impl Attempt {
167
    /// Extend this attempt by a single additional failure.
168
4
    fn grow(&mut self) {
169
4
        *self = match *self {
170
2
            Attempt::Single(idx) => Attempt::Range(idx, idx + 1),
171
2
            Attempt::Range(first, last) => Attempt::Range(first, last + 1),
172
        };
173
4
    }
174
}
175

            
176
impl<E, T> Extend<T> for RetryError<E>
177
where
178
    T: Into<E>,
179
{
180
82
    fn extend<C>(&mut self, iter: C)
181
82
    where
182
82
        C: IntoIterator<Item = T>,
183
82
    {
184
86
        for item in iter.into_iter() {
185
86
            self.push(item);
186
86
        }
187
82
    }
188
}
189

            
190
impl<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
80
    fn into_iter(self) -> Self::IntoIter {
199
80
        let v: Vec<_> = self.errors.into_iter().map(|x| x.1).collect();
200
80
        v.into_iter()
201
80
    }
202
}
203

            
204
impl Display for Attempt {
205
8
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
206
8
        match self {
207
6
            Attempt::Single(idx) => write!(f, "Attempt {}", idx),
208
2
            Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
209
        }
210
8
    }
211
}
212

            
213
impl<E: AsRef<dyn Error>> Display for RetryError<E> {
214
8
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
215
8
        match self.n_errors {
216
2
            0 => write!(f, "Unable to {}. (No errors given)", self.doing),
217
            1 => {
218
2
                write!(f, "Unable to {}: ", self.doing)?;
219
2
                fmt_error_with_sources(self.errors[0].1.as_ref(), f)
220
            }
221
4
            n => {
222
4
                write!(
223
4
                    f,
224
4
                    "Tried to {} {} times, but all attempts failed",
225
4
                    self.doing, n
226
4
                )?;
227

            
228
12
                for (attempt, e) in &self.errors {
229
8
                    write!(f, "\n{}: ", attempt)?;
230
8
                    fmt_error_with_sources(e.as_ref(), f)?;
231
                }
232
4
                Ok(())
233
            }
234
        }
235
8
    }
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
/// ```
278
3706
pub fn fmt_error_with_sources(mut e: &dyn Error, f: &mut fmt::Formatter) -> fmt::Result {
279
3706
    let mut last = String::new();
280
3706
    let mut sep = iter::once("").chain(iter::repeat(": "));
281
    loop {
282
4784
        let this = e.to_string();
283
4784
        if !last.contains(&this) {
284
4553
            write!(f, "{}{}", sep.next().expect("repeat ended"), &this)?;
285
231
        }
286
4784
        last = this;
287

            
288
4784
        if let Some(ne) = e.source() {
289
1078
            e = ne;
290
1078
        } else {
291
3706
            break;
292
3706
        }
293
3706
    }
294
3706
    Ok(())
295
3706
}
296

            
297
#[cfg(test)]
298
mod 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
            "\
331
Tried to convert some things 3 times, but all attempts failed
332
Attempt 1: provided string was not `true` or `false`
333
Attempt 2: invalid digit found in string
334
Attempt 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
            "\
398
Tried to parse some integers 3 times, but all attempts failed
399
Attempts 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
}