#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
#![warn(noop_method_call)]
#![warn(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unchecked_time_subtraction)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::mod_module_files)]
#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] #![allow(mismatched_lifetime_syntaxes)] #![allow(clippy::collapsible_if)] #![deny(clippy::unused_async)]
use std::error::Error;
use std::fmt::{self, Debug, Display, Error as FmtError, Formatter};
use std::iter;
use std::time::{Duration, SystemTime};
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use web_time::Instant;
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct RetryError<E> {
doing: String,
errors: Vec<(Attempt, E, Instant)>,
n_errors: usize,
first_error_at: Option<SystemTime>,
}
#[derive(Debug, Clone)]
enum Attempt {
Single(usize),
Range(usize, usize),
}
impl<E: Debug + AsRef<dyn Error>> Error for RetryError<E> {}
impl<E> RetryError<E> {
pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
RetryError {
doing: doing.into(),
errors: Vec::new(),
n_errors: 0,
first_error_at: None,
}
}
pub fn push_timed<T>(&mut self, err: T, instant: Instant, wall_clock: Option<SystemTime>)
where
T: Into<E>,
{
if self.n_errors < usize::MAX {
self.n_errors += 1;
let attempt = Attempt::Single(self.n_errors);
if self.first_error_at.is_none() {
self.first_error_at = wall_clock;
}
self.errors.push((attempt, err.into(), instant));
}
}
pub fn push<T>(&mut self, err: T)
where
T: Into<E>,
{
self.push_timed(err, current_instant(), Some(current_system_time()));
}
pub fn sources(&self) -> impl Iterator<Item = &E> {
self.errors.iter().map(|(.., e, _)| e)
}
pub fn len(&self) -> usize {
self.errors.len()
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
#[allow(clippy::disallowed_methods)] pub fn extend<T>(&mut self, iter: impl IntoIterator<Item = T>)
where
T: Into<E>,
{
for item in iter {
self.push(item);
}
}
pub fn dedup_by<F>(&mut self, same_err: F)
where
F: Fn(&E, &E) -> bool,
{
let mut old_errs = Vec::new();
std::mem::swap(&mut old_errs, &mut self.errors);
for (attempt, err, timestamp) in old_errs {
if let Some((last_attempt, last_err, ..)) = self.errors.last_mut() {
if same_err(last_err, &err) {
last_attempt.grow(attempt.count());
} else {
self.errors.push((attempt, err, timestamp));
}
} else {
self.errors.push((attempt, err, timestamp));
}
}
}
pub fn extend_from_retry_error(&mut self, other: RetryError<E>) {
if self.first_error_at.is_none() {
self.first_error_at = other.first_error_at;
}
for (attempt, err, timestamp) in other.errors {
let Some(new_n_errors) = self.n_errors.checked_add(attempt.count()) else {
break;
};
let new_attempt = match attempt {
Attempt::Single(_) => Attempt::Single(new_n_errors),
Attempt::Range(_, _) => Attempt::Range(self.n_errors + 1, new_n_errors),
};
self.errors.push((new_attempt, err, timestamp));
self.n_errors = new_n_errors;
}
}
}
impl<E: PartialEq<E>> RetryError<E> {
pub fn dedup(&mut self) {
self.dedup_by(PartialEq::eq);
}
}
impl Attempt {
fn grow(&mut self, count: usize) {
*self = match *self {
Attempt::Single(idx) => Attempt::Range(idx, idx + count),
Attempt::Range(first, last) => Attempt::Range(first, last + count),
};
}
fn count(&self) -> usize {
match *self {
Attempt::Single(_) => 1,
Attempt::Range(first, last) => last - first + 1,
}
}
}
impl<E> IntoIterator for RetryError<E> {
type Item = E;
type IntoIter = std::vec::IntoIter<E>;
#[allow(clippy::needless_collect)]
fn into_iter(self) -> Self::IntoIter {
self.errors
.into_iter()
.map(|(.., e, _)| e)
.collect::<Vec<_>>()
.into_iter()
}
}
impl Display for Attempt {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
match self {
Attempt::Single(idx) => write!(f, "Attempt {}", idx),
Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
}
}
}
impl<E: AsRef<dyn Error>> Display for RetryError<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
let show_timestamps = f.alternate();
match self.n_errors {
0 => write!(f, "Unable to {}. (No errors given)", self.doing),
1 => {
write!(f, "Unable to {}", self.doing)?;
if show_timestamps {
if let (Some((.., timestamp)), Some(first_at)) =
(self.errors.first(), self.first_error_at)
{
write!(
f,
" at {} ({})",
humantime::format_rfc3339(first_at),
FormatTimeAgo(timestamp.elapsed())
)?;
}
}
write!(f, ": ")?;
fmt_error_with_sources(self.errors[0].1.as_ref(), f)
}
n => {
write!(
f,
"Tried to {} {} times, but all attempts failed",
self.doing, n
)?;
if show_timestamps {
if let (Some(first_at), Some((.., first_ts)), Some((.., last_ts))) =
(self.first_error_at, self.errors.first(), self.errors.last())
{
let duration = last_ts.saturating_duration_since(*first_ts);
write!(f, " (from {} ", humantime::format_rfc3339(first_at))?;
if duration.as_secs() > 0 {
write!(f, "to {}", humantime::format_rfc3339(first_at + duration))?;
}
write!(f, ", {})", FormatTimeAgo(last_ts.elapsed()))?;
}
}
let first_ts = self.errors.first().map(|(.., ts)| ts);
for (attempt, e, timestamp) in &self.errors {
write!(f, "\n{}", attempt)?;
if show_timestamps {
if let Some(first_ts) = first_ts {
let offset = timestamp.saturating_duration_since(*first_ts);
if offset.as_secs() > 0 {
write!(f, " (+{})", FormatDuration(offset))?;
}
}
}
write!(f, ": ")?;
fmt_error_with_sources(e.as_ref(), f)?;
}
Ok(())
}
}
}
}
struct FormatDuration(Duration);
impl Display for FormatDuration {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
fmt_duration_impl(self.0, f)
}
}
struct FormatTimeAgo(Duration);
impl Display for FormatTimeAgo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let secs = self.0.as_secs();
let millis = self.0.as_millis();
if secs == 0 && millis == 0 {
return write!(f, "just now");
}
fmt_duration_impl(self.0, f)?;
write!(f, " ago")
}
}
fn fmt_duration_impl(duration: Duration, f: &mut Formatter<'_>) -> fmt::Result {
let secs = duration.as_secs();
if secs == 0 {
let millis = duration.as_millis();
if millis == 0 {
write!(f, "0s")
} else {
write!(f, "{}ms", millis)
}
} else if secs < 60 {
write!(f, "{}s", secs)
} else if secs < 3600 {
let mins = secs / 60;
let rem_secs = secs % 60;
if rem_secs == 0 {
write!(f, "{}m", mins)
} else {
write!(f, "{}m {}s", mins, rem_secs)
}
} else {
let hours = secs / 3600;
let mins = (secs % 3600) / 60;
if mins == 0 {
write!(f, "{}h", hours)
} else {
write!(f, "{}h {}m", hours, mins)
}
}
}
pub fn fmt_error_with_sources(mut e: &dyn Error, f: &mut fmt::Formatter) -> fmt::Result {
let mut last = String::new();
let mut sep = iter::once("").chain(iter::repeat(": "));
loop {
let this = e.to_string();
if !last.contains(&this) {
write!(f, "{}{}", sep.next().expect("repeat ended"), &this)?;
}
last = this;
if let Some(ne) = e.source() {
e = ne;
} else {
break;
}
}
Ok(())
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
fn current_system_time() -> SystemTime {
use web_time::web::SystemTimeExt as _;
web_time::SystemTime::now().to_std()
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
fn current_system_time() -> SystemTime {
#![allow(clippy::disallowed_methods)]
SystemTime::now()
}
fn current_instant() -> Instant {
#![allow(clippy::disallowed_methods)]
Instant::now()
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::disallowed_methods)]
use super::*;
use derive_more::From;
#[test]
fn bad_parse1() {
let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
if let Err(e) = "maybe".parse::<bool>() {
err.push(e);
}
if let Err(e) = "a few".parse::<u32>() {
err.push(e);
}
if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
err.push(e);
}
let disp = format!("{}", err);
assert_eq!(
disp,
"\
Tried to convert some things 3 times, but all attempts failed
Attempt 1: provided string was not `true` or `false`
Attempt 2: invalid digit found in string
Attempt 3: invalid IP address syntax"
);
let disp_alt = format!("{:#}", err);
assert!(disp_alt.contains("Tried to convert some things 3 times, but all attempts failed"));
assert!(disp_alt.contains("(from 20")); }
#[test]
fn no_problems() {
let empty: RetryError<anyhow::Error> =
RetryError::in_attempt_to("immanentize the eschaton");
let disp = format!("{}", empty);
assert_eq!(
disp,
"Unable to immanentize the eschaton. (No errors given)"
);
}
#[test]
fn one_problem() {
let mut err: RetryError<anyhow::Error> =
RetryError::in_attempt_to("connect to torproject.org");
if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
err.push(e);
}
let disp = format!("{}", err);
assert_eq!(
disp,
"Unable to connect to torproject.org: invalid IP address syntax"
);
let disp_alt = format!("{:#}", err);
assert!(disp_alt.contains("Unable to connect to torproject.org at 20")); assert!(disp_alt.contains("invalid IP address syntax"));
}
#[test]
fn operations() {
use std::num::ParseIntError;
#[derive(From, Clone, Debug, Eq, PartialEq)]
struct Wrapper(ParseIntError);
impl AsRef<dyn Error + 'static> for Wrapper {
fn as_ref(&self) -> &(dyn Error + 'static) {
&self.0
}
}
let mut err: RetryError<Wrapper> = RetryError::in_attempt_to("parse some integers");
assert!(err.is_empty());
assert_eq!(err.len(), 0);
err.extend(
vec!["not", "your", "number"]
.iter()
.filter_map(|s| s.parse::<u16>().err())
.map(Wrapper),
);
assert!(!err.is_empty());
assert_eq!(err.len(), 3);
let cloned = err.clone();
for (s1, s2) in err.sources().zip(cloned.sources()) {
assert_eq!(s1, s2);
}
err.dedup();
let disp = format!("{}", err);
assert_eq!(
disp,
"\
Tried to parse some integers 3 times, but all attempts failed
Attempts 1..3: invalid digit found in string"
);
let disp_alt = format!("{:#}", err);
assert!(disp_alt.contains("Tried to parse some integers 3 times, but all attempts failed"));
assert!(disp_alt.contains("(from 20")); }
#[test]
fn overflow() {
use std::num::ParseIntError;
let mut err: RetryError<ParseIntError> =
RetryError::in_attempt_to("parse too many integers");
assert!(err.is_empty());
let mut errors: Vec<ParseIntError> = vec!["no", "numbers"]
.iter()
.filter_map(|s| s.parse::<u16>().err())
.collect();
err.n_errors = usize::MAX;
err.errors.push((
Attempt::Range(1, err.n_errors),
errors.pop().expect("parser did not fail"),
Instant::now(),
));
assert!(err.n_errors == usize::MAX);
assert!(err.len() == 1);
err.push(errors.pop().expect("parser did not fail"));
assert!(err.n_errors == usize::MAX);
assert!(err.len() == 1);
}
#[test]
fn extend_from_retry_preserve_timestamps() {
let n1 = Instant::now();
let n2 = n1 + Duration::from_secs(10);
let n3 = n1 + Duration::from_secs(20);
let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do first thing");
let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do second thing");
err2.push_timed(anyhow::Error::msg("e1"), n1, None);
err2.push_timed(anyhow::Error::msg("e2"), n2, None);
assert!(err1.first_error_at.is_none());
err1.extend_from_retry_error(err2);
assert_eq!(err1.len(), 2);
assert_eq!(err1.errors[0].2, n1);
assert_eq!(err1.errors[1].2, n2);
err1.push_timed(anyhow::Error::msg("e3"), n3, None);
assert_eq!(err1.len(), 3);
assert_eq!(err1.errors[2].2, n3);
}
#[test]
fn extend_from_retry_preserve_ranges() {
let n1 = Instant::now();
let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 1");
err1.push(anyhow::Error::msg("e1"));
err1.push(anyhow::Error::msg("e2"));
assert_eq!(err1.n_errors, 2);
let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to("do thing 2");
err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
err2.push_timed(anyhow::Error::msg("repeated"), n1, None);
err2.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
assert_eq!(err2.len(), 1); match err2.errors[0].0 {
Attempt::Range(1, 3) => {}
_ => panic!("Expected range 1..3"),
}
err1.extend_from_retry_error(err2);
assert_eq!(err1.len(), 3); assert_eq!(err1.n_errors, 5);
match err1.errors[2].0 {
Attempt::Range(3, 5) => {}
ref x => panic!("Expected range 3..5, got {:?}", x),
}
}
#[test]
fn dedup_after_extend_same_doing() {
let doing = "do thing";
let message = "error";
let n1 = Instant::now();
let mut err1: RetryError<anyhow::Error> = RetryError::in_attempt_to(doing);
err1.push(anyhow::Error::msg(message));
assert_eq!(err1.n_errors, 1);
let mut err2: RetryError<anyhow::Error> = RetryError::in_attempt_to(doing);
err2.push_timed(anyhow::Error::msg(message), n1, None);
err2.push_timed(anyhow::Error::msg(message), n1, None);
err2.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
assert_eq!(err2.len(), 1); match err2.errors[0].0 {
Attempt::Range(1, 2) => {}
_ => panic!("Expected range 1..2"),
}
err1.extend_from_retry_error(err2);
assert_eq!(err1.len(), 2); assert_eq!(err1.n_errors, 3);
err1.dedup_by(|e1, e2| e1.to_string() == e2.to_string());
assert_eq!(err1.len(), 1); assert_eq!(err1.n_errors, 3);
match err1.errors[0].0 {
Attempt::Range(1, 3) => {}
ref x => panic!("Expected range 1..3, got {:?}", x),
}
}
}