pax_global_header00006660000000000000000000000064143113277670014525gustar00rootroot0000000000000052 comment=616a402bf4030ff9dcdd06354d66a43f6cb64ecf retry-2.0.0/000077500000000000000000000000001431132776700126715ustar00rootroot00000000000000retry-2.0.0/.gitignore000066400000000000000000000000261431132776700146570ustar00rootroot00000000000000doc target Cargo.lock retry-2.0.0/.travis.yml000066400000000000000000000001721431132776700150020ustar00rootroot00000000000000language: "rust" rust: - "stable" - "beta" matrix: allow_failures: - rust: "beta" notifications: email: false retry-2.0.0/Cargo.toml000066400000000000000000000010271431132776700146210ustar00rootroot00000000000000[package] authors = ["Jimmy Cuadra ", "Sam Rijs "] description = "Utilities for retrying operations that can fail." documentation = "https://docs.rs/retry" edition = "2018" homepage = "https://github.com/jimmycuadra/retry" keywords = ["utility", "utilities"] license = "MIT" name = "retry" readme = "README.md" repository = "https://github.com/jimmycuadra/retry" version = "2.0.0" [dependencies] rand = { version = "^0.8", optional = true } [features] default = ["random"] random = ["rand"] retry-2.0.0/LICENSE000066400000000000000000000020621431132776700136760ustar00rootroot00000000000000Copyright (c) 2015-2020 Jimmy Cuadra and Sam Rijs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. retry-2.0.0/README.md000066400000000000000000000005631431132776700141540ustar00rootroot00000000000000# retry [![Build Status](https://travis-ci.org/jimmycuadra/retry.svg?branch=master)](https://travis-ci.org/jimmycuadra/retry) Crate `retry` provides utilities for retrying operations that can fail. ## Documentation retry has [comprehensive documentation](https://docs.rs/retry) available on docs.rs. ## Legal retry is released under the MIT license. See `LICENSE`. retry-2.0.0/src/000077500000000000000000000000001431132776700134605ustar00rootroot00000000000000retry-2.0.0/src/delay/000077500000000000000000000000001431132776700145565ustar00rootroot00000000000000retry-2.0.0/src/delay/mod.rs000066400000000000000000000126351431132776700157120ustar00rootroot00000000000000//! Different types of delay for retryable operations. use std::time::Duration; use std::u64::MAX as U64_MAX; #[cfg(feature = "random")] mod random; #[cfg(feature = "random")] pub use random::{jitter, Range}; /// Each retry increases the delay since the last exponentially. #[derive(Debug)] pub struct Exponential { current: u64, factor: f64, } impl Exponential { /// Create a new [`Exponential`] using the given millisecond duration as the initial delay and /// an exponential backoff factor of `2.0`. pub fn from_millis(base: u64) -> Self { Exponential { current: base, factor: 2.0, } } /// Create a new [`Exponential`] using the given millisecond duration as the initial delay and /// the same duration as the exponential backoff factor. This was the behavior of /// [`Exponential::from_millis`] prior to version 2.0. pub fn from_millis_with_base_factor(base: u64) -> Self { Exponential { current: base, factor: base as f64, } } /// Create a new [`Exponential`] using the given millisecond duration as the initial delay and /// the given exponential backoff factor. pub fn from_millis_with_factor(base: u64, factor: f64) -> Self { Exponential { current: base, factor, } } } impl Iterator for Exponential { type Item = Duration; fn next(&mut self) -> Option { let duration = Duration::from_millis(self.current); let next = (self.current as f64) * self.factor; self.current = if next > (U64_MAX as f64) { U64_MAX } else { next as u64 }; Some(duration) } } impl From for Exponential { fn from(duration: Duration) -> Self { Self::from_millis(duration.as_millis() as u64) } } #[test] fn exponential_with_factor() { let mut iter = Exponential::from_millis_with_factor(1000, 2.0); assert_eq!(iter.next(), Some(Duration::from_millis(1000))); assert_eq!(iter.next(), Some(Duration::from_millis(2000))); assert_eq!(iter.next(), Some(Duration::from_millis(4000))); assert_eq!(iter.next(), Some(Duration::from_millis(8000))); assert_eq!(iter.next(), Some(Duration::from_millis(16000))); assert_eq!(iter.next(), Some(Duration::from_millis(32000))); } #[test] fn exponential_overflow() { let mut iter = Exponential::from_millis(U64_MAX); assert_eq!(iter.next(), Some(Duration::from_millis(U64_MAX))); assert_eq!(iter.next(), Some(Duration::from_millis(U64_MAX))); } /// Each retry uses a delay which is the sum of the two previous delays. /// /// Depending on the problem at hand, a fibonacci delay strategy might perform better and lead to /// better throughput than the [`Exponential`] strategy. /// /// See ["A Performance Comparison of Different Backoff Algorithms under Different Rebroadcast /// Probabilities for MANETs"](https://www.researchgate.net/publication/255672213_A_Performance_Comparison_of_Different_Backoff_Algorithms_under_Different_Rebroadcast_Probabilities_for_MANET's) /// for more details. #[derive(Debug)] pub struct Fibonacci { curr: u64, next: u64, } impl Fibonacci { /// Create a new [`Fibonacci`] using the given duration in milliseconds. pub fn from_millis(millis: u64) -> Fibonacci { Fibonacci { curr: millis, next: millis, } } } impl Iterator for Fibonacci { type Item = Duration; fn next(&mut self) -> Option { let duration = Duration::from_millis(self.curr); if let Some(next_next) = self.curr.checked_add(self.next) { self.curr = self.next; self.next = next_next; } else { self.curr = self.next; self.next = U64_MAX; } Some(duration) } } impl From for Fibonacci { fn from(duration: Duration) -> Self { Self::from_millis(duration.as_millis() as u64) } } #[test] fn fibonacci() { let mut iter = Fibonacci::from_millis(10); assert_eq!(iter.next(), Some(Duration::from_millis(10))); assert_eq!(iter.next(), Some(Duration::from_millis(10))); assert_eq!(iter.next(), Some(Duration::from_millis(20))); assert_eq!(iter.next(), Some(Duration::from_millis(30))); assert_eq!(iter.next(), Some(Duration::from_millis(50))); assert_eq!(iter.next(), Some(Duration::from_millis(80))); } #[test] fn fibonacci_saturated() { let mut iter = Fibonacci::from_millis(U64_MAX); assert_eq!(iter.next(), Some(Duration::from_millis(U64_MAX))); assert_eq!(iter.next(), Some(Duration::from_millis(U64_MAX))); } /// Each retry uses a fixed delay. #[derive(Debug)] pub struct Fixed { duration: Duration, } impl Fixed { /// Create a new [`Fixed`] using the given duration in milliseconds. pub fn from_millis(millis: u64) -> Self { Fixed { duration: Duration::from_millis(millis), } } } impl Iterator for Fixed { type Item = Duration; fn next(&mut self) -> Option { Some(self.duration) } } impl From for Fixed { fn from(delay: Duration) -> Self { Self { duration: delay.into(), } } } /// Each retry happens immediately without any delay. #[derive(Debug)] pub struct NoDelay; impl Iterator for NoDelay { type Item = Duration; fn next(&mut self) -> Option { Some(Duration::default()) } } retry-2.0.0/src/delay/random.rs000066400000000000000000000053371431132776700164140ustar00rootroot00000000000000use std::{ ops::{Range as StdRange, RangeInclusive}, time::Duration, }; use rand::{ distributions::{Distribution, Uniform}, random, rngs::ThreadRng, thread_rng, }; /// Each retry uses a duration randomly chosen from a range. (When the `random` Cargo feature is /// enabled.) #[derive(Debug)] pub struct Range { distribution: Uniform, rng: ThreadRng, } impl Range { /// Create a new [`Range`] between the given millisecond durations, excluding the maximum value. /// /// # Panics /// /// Panics if the minimum is greater than or equal to the maximum. pub fn from_millis_exclusive(minimum: u64, maximum: u64) -> Self { Range { distribution: Uniform::new(minimum, maximum), rng: thread_rng(), } } /// Create a new [`Range`] between the given millisecond durations, including the maximum value. /// /// # Panics /// /// Panics if the minimum is greater than or equal to the maximum. pub fn from_millis_inclusive(minimum: u64, maximum: u64) -> Self { Range { distribution: Uniform::new_inclusive(minimum, maximum), rng: thread_rng(), } } } impl Iterator for Range { type Item = Duration; fn next(&mut self) -> Option { Some(Duration::from_millis( self.distribution.sample(&mut self.rng), )) } } impl From> for Range { fn from(range: StdRange) -> Self { Self::from_millis_exclusive(range.start.as_millis() as u64, range.end.as_millis() as u64) } } impl From> for Range { fn from(range: RangeInclusive) -> Self { Self::from_millis_inclusive( range.start().as_millis() as u64, range.end().as_millis() as u64, ) } } /// Apply full random jitter to a duration. (When the `random` Cargo feature is enabled.) pub fn jitter(duration: Duration) -> Duration { let jitter = random::(); let secs = ((duration.as_secs() as f64) * jitter).ceil() as u64; let nanos = ((f64::from(duration.subsec_nanos())) * jitter).ceil() as u32; Duration::new(secs, nanos) } #[test] fn range_uniform() { let mut range = Range::from_millis_exclusive(0, 1); assert_eq!(Duration::from_millis(0), range.next().unwrap()); assert_eq!(Duration::from_millis(0), range.next().unwrap()); assert_eq!(Duration::from_millis(0), range.next().unwrap()); } #[test] #[should_panic] fn range_uniform_wrong_input() { Range::from_millis_exclusive(0, 0); } #[test] fn test_jitter() { assert_eq!(Duration::from_millis(0), jitter(Duration::from_millis(0))); assert!(Duration::from_millis(0) < jitter(Duration::from_millis(2))); } retry-2.0.0/src/lib.rs000066400000000000000000000260271431132776700146030ustar00rootroot00000000000000//! Crate `retry` provides utilities for retrying operations that can fail. //! //! # Usage //! //! Retry an operation using the [`retry`] function. [`retry`] accepts an iterator over //! [`Duration`]s and a closure that returns a [`Result`] (or [`OperationResult`]; see below). The //! iterator is used to determine how long to wait after each unsuccessful try and how many times to //! try before giving up and returning [`Result::Err`]. The closure determines either the final //! successful value, or an error value, which can either be returned immediately or used to //! indicate that the operation should be retried. //! //! Any type that implements [`Iterator`] with an associated `Item` type of [`Duration`] can be //! used to determine retry behavior, though a few useful implementations are provided in the //! [`delay`] module, including a fixed delay and exponential backoff. //! //! ``` //! # use retry::retry; //! # use retry::delay::Fixed; //! let mut collection = vec![1, 2, 3].into_iter(); //! //! let result = retry(Fixed::from_millis(100), || { //! match collection.next() { //! Some(n) if n == 3 => Ok("n is 3!"), //! Some(_) => Err("n must be 3!"), //! None => Err("n was never 3!"), //! } //! }); //! //! assert!(result.is_ok()); //! ``` //! //! The [`Iterator`] API can be used to limit or modify the delay strategy. For example, to limit //! the number of retries to 1: //! //! ``` //! # use retry::retry; //! # use retry::delay::Fixed; //! let mut collection = vec![1, 2, 3].into_iter(); //! //! let result = retry(Fixed::from_millis(100).take(1), || { //! match collection.next() { //! Some(n) if n == 3 => Ok("n is 3!"), //! Some(_) => Err("n must be 3!"), //! None => Err("n was never 3!"), //! } //! }); //! //! assert!(result.is_err()); //! ``` //! #![cfg_attr( feature = "random", doc = r##" To apply random jitter to any delay strategy, the [`delay::jitter`] function can be used in combination with the [`Iterator`] API: ``` # use retry::retry; # use retry::delay::{Exponential, jitter}; let mut collection = vec![1, 2, 3].into_iter(); let result = retry(Exponential::from_millis(10).map(jitter).take(3), || { match collection.next() { Some(n) if n == 3 => Ok("n is 3!"), Some(_) => Err("n must be 3!"), None => Err("n was never 3!"), } }); assert!(result.is_ok()); ``` "## )] //! //! To deal with fatal errors, return [`OperationResult`], which is like [`Result`], but with a //! third case to distinguish between errors that should cause a retry and errors that should //! immediately return, halting retry behavior. (Internally, [`OperationResult`] is always used, and //! closures passed to [`retry`] that return plain [`Result`] are converted into //! [`OperationResult`].) //! //! ``` //! # use retry::retry; //! # use retry::delay::Fixed; //! use retry::OperationResult; //! let mut collection = vec![1, 2].into_iter(); //! let value = retry(Fixed::from_millis(1), || { //! match collection.next() { //! Some(n) if n == 2 => OperationResult::Ok(n), //! Some(_) => OperationResult::Retry("not 2"), //! None => OperationResult::Err("not found"), //! } //! }).unwrap(); //! //! assert_eq!(value, 2); //! ``` //! //! If your operation needs to know how many times it's been tried, use the [`retry_with_index`] //! function. This works the same as [`retry`], but passes the number of the current try to the //! closure as an argument. //! //! ``` //! # use retry::retry_with_index; //! # use retry::delay::Fixed; //! # use retry::OperationResult; //! let mut collection = vec![1, 2, 3, 4, 5].into_iter(); //! //! let result = retry_with_index(Fixed::from_millis(100), |current_try| { //! if current_try > 3 { //! return OperationResult::Err("did not succeed within 3 tries"); //! } //! //! match collection.next() { //! Some(n) if n == 5 => OperationResult::Ok("n is 5!"), //! Some(_) => OperationResult::Retry("n must be 5!"), //! None => OperationResult::Retry("n was never 5!"), //! } //! }); //! //! assert!(result.is_err()); //! ``` //! //! # Features //! //! - `random`: offer some random delay utilities (on by default) #![deny(missing_debug_implementations, missing_docs, warnings)] use std::{ error::Error as StdError, fmt::{Display, Error as FmtError, Formatter}, thread::sleep, time::Duration, }; pub mod delay; mod opresult; #[doc(inline)] pub use opresult::OperationResult; /// Retry the given operation synchronously until it succeeds, or until the given [`Duration`] /// iterator ends. pub fn retry(iterable: I, mut operation: O) -> Result> where I: IntoIterator, O: FnMut() -> OR, OR: Into>, { retry_with_index(iterable, |_| operation()) } /// Retry the given operation synchronously until it succeeds, or until the given [`Duration`] /// iterator ends, with each iteration of the operation receiving the number of the attempt as an /// argument. pub fn retry_with_index(iterable: I, mut operation: O) -> Result> where I: IntoIterator, O: FnMut(u64) -> OR, OR: Into>, { let mut iterator = iterable.into_iter(); let mut current_try = 1; let mut total_delay = Duration::default(); loop { match operation(current_try).into() { OperationResult::Ok(value) => return Ok(value), OperationResult::Retry(error) => { if let Some(delay) = iterator.next() { sleep(delay); current_try += 1; total_delay += delay; } else { return Err(Error { error, total_delay, tries: current_try, }); } } OperationResult::Err(error) => { return Err(Error { error, total_delay, tries: current_try, }); } } } } /// An error with a retryable operation. #[derive(Debug, PartialEq, Eq)] pub struct Error { /// The error returned by the operation on the last try. pub error: E, /// The duration spent waiting between retries of the operation. /// /// Note that this does not include the time spent running the operation itself. pub total_delay: Duration, /// The total number of times the operation was tried. pub tries: u64, } impl Display for Error where E: Display, { fn fmt(&self, formatter: &mut Formatter) -> Result<(), FmtError> { Display::fmt(&self.error, formatter) } } impl StdError for Error where E: StdError, { #[allow(deprecated)] fn description(&self) -> &str { self.error.description() } fn cause(&self) -> Option<&dyn StdError> { Some(&self.error) } } #[cfg(test)] mod tests { use std::time::Duration; use super::delay::{Exponential, Fixed, NoDelay}; use super::opresult::OperationResult; use super::{retry, retry_with_index, Error}; #[test] fn succeeds_with_infinite_retries() { let mut collection = vec![1, 2, 3, 4, 5].into_iter(); let value = retry(NoDelay, || match collection.next() { Some(n) if n == 5 => Ok(n), Some(_) => Err("not 5"), None => Err("not 5"), }) .unwrap(); assert_eq!(value, 5); } #[test] fn succeeds_with_maximum_retries() { let mut collection = vec![1, 2].into_iter(); let value = retry(NoDelay.take(1), || match collection.next() { Some(n) if n == 2 => Ok(n), Some(_) => Err("not 2"), None => Err("not 2"), }) .unwrap(); assert_eq!(value, 2); } #[test] fn fails_after_last_try() { let mut collection = vec![1].into_iter(); let res = retry(NoDelay.take(1), || match collection.next() { Some(n) if n == 2 => Ok(n), Some(_) => Err("not 2"), None => Err("not 2"), }); assert_eq!( res, Err(Error { error: "not 2", tries: 2, total_delay: Duration::from_millis(0) }) ); } #[test] fn fatal_errors() { let mut collection = vec![1].into_iter(); let res = retry(NoDelay.take(2), || match collection.next() { Some(n) if n == 2 => OperationResult::Ok(n), Some(_) => OperationResult::Err("no retry"), None => OperationResult::Err("not 2"), }); assert_eq!( res, Err(Error { error: "no retry", tries: 1, total_delay: Duration::from_millis(0) }) ); } #[test] fn succeeds_with_fixed_delay() { let mut collection = vec![1, 2].into_iter(); let value = retry(Fixed::from_millis(1), || match collection.next() { Some(n) if n == 2 => Ok(n), Some(_) => Err("not 2"), None => Err("not 2"), }) .unwrap(); assert_eq!(value, 2); } #[test] fn fixed_delay_from_duration() { assert_eq!( Fixed::from_millis(1_000).next(), Fixed::from(Duration::from_secs(1)).next(), ); } #[test] fn succeeds_with_exponential_delay() { let mut collection = vec![1, 2].into_iter(); let value = retry(Exponential::from_millis(1), || match collection.next() { Some(n) if n == 2 => Ok(n), Some(_) => Err("not 2"), None => Err("not 2"), }) .unwrap(); assert_eq!(value, 2); } #[test] fn succeeds_with_exponential_delay_with_factor() { let mut collection = vec![1, 2].into_iter(); let value = retry( Exponential::from_millis_with_factor(1000, 2.0), || match collection.next() { Some(n) if n == 2 => Ok(n), Some(_) => Err("not 2"), None => Err("not 2"), }, ) .unwrap(); assert_eq!(value, 2); } #[test] #[cfg(feature = "random")] fn succeeds_with_ranged_delay() { use super::delay::Range; let mut collection = vec![1, 2].into_iter(); let value = retry(Range::from_millis_exclusive(1, 10), || { match collection.next() { Some(n) if n == 2 => Ok(n), Some(_) => Err("not 2"), None => Err("not 2"), } }) .unwrap(); assert_eq!(value, 2); } #[test] fn succeeds_with_index() { let mut collection = vec![1, 2, 3].into_iter(); let value = retry_with_index(NoDelay, |current_try| match collection.next() { Some(n) if n == current_try => Ok(n), Some(_) => Err("not current_try"), None => Err("not current_try"), }) .unwrap(); assert_eq!(value, 1); } } retry-2.0.0/src/opresult.rs000066400000000000000000000061671431132776700157150ustar00rootroot00000000000000//! Provides a ternary result for operations. //! //! # Examples //! //! ```rust //! # use retry::retry; //! # use retry::delay::Fixed; //! use retry::OperationResult; //! let mut collection = vec![1, 2].into_iter(); //! let value = retry(Fixed::from_millis(1), || { //! match collection.next() { //! Some(n) if n == 2 => OperationResult::Ok(n), //! Some(_) => OperationResult::Retry("not 2"), //! None => OperationResult::Err("not found"), //! } //! }).unwrap(); //! //! assert_eq!(value, 2); //! ``` /// A result that represents either success, retryable failure, or immediately-returning failure. #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] pub enum OperationResult { /// Contains the success value. Ok(T), /// Contains the error value if duration is exceeded. Retry(E), /// Contains an error value to return immediately. Err(E), } impl From> for OperationResult { fn from(item: Result) -> Self { match item { Ok(v) => OperationResult::Ok(v), Err(e) => OperationResult::Retry(e), } } } impl OperationResult { /// Returns `true` if the result is [`OperationResult::Ok`]. /// /// # Examples /// /// Basic usage: /// /// ``` /// # use retry::OperationResult; /// /// let x: OperationResult = OperationResult::Ok(-3); /// assert_eq!(x.is_ok(), true); /// /// let x: OperationResult = OperationResult::Retry("Some error message"); /// assert_eq!(x.is_ok(), false); /// /// let x: OperationResult = OperationResult::Err("Some other error message"); /// assert_eq!(x.is_ok(), false); /// ``` pub fn is_ok(&self) -> bool { matches!(self, Self::Ok(_)) } /// Returns `true` if the result is [`OperationResult::Retry`]. /// /// # Examples /// /// Basic usage: /// /// ``` /// # use retry::OperationResult; /// /// let x: OperationResult = OperationResult::Ok(-3); /// assert_eq!(x.is_retry(), false); /// /// let x: OperationResult = OperationResult::Retry("Some error message"); /// assert_eq!(x.is_retry(), true); /// /// let x: OperationResult = OperationResult::Err("Some other error message"); /// assert_eq!(x.is_retry(), false); /// ``` pub fn is_retry(&self) -> bool { matches!(self, Self::Retry(_)) } /// Returns `true` if the result is [`OperationResult::Err`]. /// /// # Examples /// /// Basic usage: /// /// ``` /// # use retry::OperationResult; /// /// let x: OperationResult = OperationResult::Ok(-3); /// assert_eq!(x.is_err(), false); /// /// let x: OperationResult = OperationResult::Retry("Some error message"); /// assert_eq!(x.is_err(), false); /// /// let x: OperationResult = OperationResult::Err("Some other error message"); /// assert_eq!(x.is_err(), true); /// ``` pub fn is_err(&self) -> bool { matches!(self, Self::Err(_)) } }