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