retry_block/lib.rs
1//! `retry-block` provides utilities to retry operations that may fail with configurable backoff behavior.
2//!
3//! # Usage
4//!
5//! Retry an operation using the corresponding `retry` macro or `retry_fn` function. The macro
6//! accepts an iterator over `Duration`s and a block that returns a `Result` (or `OperationResult`;
7//! see below). The iterator is used to determine how long to wait after each unsuccessful try and
8//! how many times to try before giving up and returning `Result::Err`. The block determines either
9//! the final successful value, or an error value, which can either be returned immediately or used
10//! to indicate that the operation should be retried.
11//!
12//! Any type that implements `IntoIterator<Item = Duration>` can be used to determine retry behavior,
13//! though a few useful implementations are provided in the `delay` module, including a fixed delay
14//! and exponential back-off.
15//!
16//!
17//! The `Iterator` API can be used to limit or modify the delay strategy. For example, to limit the
18//! number of retries to 1:
19//!
20//! ```
21//! # use retry_block::retry;
22//! # use retry_block::delay::Fixed;
23//! # use std::time::Duration;
24//! # use retry_block::OperationResult;
25//!
26//! let mut collection = vec![1, 2, 3].into_iter();
27//!
28//! let result = retry!(Fixed::new(Duration::from_millis(100)).take(1), {
29//! match collection.next() {
30//! Some(n) if n == 3 => Ok("n is 3!"),
31//! Some(_) => Err("n must be 3!"),
32//! None => Err("n was never 3!"),
33//! }
34//! });
35//!
36//! assert!(result.is_err());
37//! ```
38//!
39#![cfg_attr(
40 feature = "config",
41 doc = r##"
42The RetryConfig struct can be used to retry an operation with a serializable retry config
43that specifies an amount of retries and a random backoff interval.
44
45```
46# use retry_block::OperationResult;
47# use retry_block::RetryConfig;
48# use retry_block::delay::Fixed;
49# use retry_block::retry;
50
51let config = RetryConfig {
52 count: 1,
53 min_backoff: 100,
54 max_backoff: 300,
55};
56let mut collection = vec![1, 2, 3].into_iter();
57
58let result = retry!(config, {
59 match collection.next() {
60 Some(n) if n == 3 => Ok("n is 3!"),
61 Some(_) => Err("n must be 3!"),
62 None => Err("n was never 3!"),
63 }
64});
65
66assert!(result.is_err());
67```
68"##
69)]
70#![cfg_attr(
71 feature = "random",
72 doc = r##"
73Random jitter is applied by default to any delay strategy, but you can make it fixed using `exact`
74or add random jitter to any delay strategy using the `jitter` function:
75
76```
77# use retry_block::retry_fn;
78# use retry_block::OperationResult;
79# use retry_block::delay::{Exponential, jitter};
80# use std::time::Duration;
81
82let mut collection = vec![1, 2, 3].into_iter();
83
84let result = retry_fn(Exponential::exact(Duration::from_millis(10)).map(jitter).take(3), || {
85 match collection.next() {
86 Some(n) if n == 3 => Ok("n is 3!"),
87 Some(_) => Err("n must be 3!"),
88 None => Err("n was never 3!"),
89 }
90});
91
92assert!(result.is_ok());
93```
94"##
95)]
96//!
97//! To deal with fatal errors, return `retry_block::OperationResult`, which is like std's `Result`, but
98//! with a third case to distinguish between errors that should cause a retry and errors that
99//! should immediately return, halting retry behavior. (Internally, `OperationResult` is always
100//! used, and closures passed to `retry` that return plain `Result` are converted into
101//! `OperationResult`.)
102//!
103//! ```
104//! # use retry_block::retry;
105//! # use retry_block::delay::Fixed;
106//! # use retry_block::OperationResult;
107//! # use std::time::Duration;
108//!
109//! let mut collection = vec![1, 2].into_iter();
110//! let value = retry!(Fixed::new(Duration::from_millis(1)), {
111//! match collection.next() {
112//! Some(n) if n == 2 => OperationResult::Ok(n),
113//! Some(_) => OperationResult::Retry("not 2"),
114//! None => OperationResult::Err("not found"),
115//! }
116//! }).unwrap();
117//!
118//! assert_eq!(value, 2);
119//! ```
120//!
121//! # Features
122//!
123//! - `random`: offer some random delay utilities (on by default)
124//! - `config`: offer serializable retry config (on by default)
125//! - `future`: offer asynchronous retry mechanisms (on by default)
126
127use serde::Deserialize;
128use std::time::Duration;
129
130pub mod delay;
131#[cfg(feature = "future")]
132pub mod future;
133mod r#macro;
134pub mod persist;
135
136pub use future::*;
137
138/// A serializable retry configuration for a random range and finite retry count
139#[derive(Debug, Deserialize, Clone)]
140pub struct RetryConfig {
141 /// how many times will we retry the operation
142 pub count: usize,
143 /// the minimum amount of milliseconds to wait before retrying
144 pub min_backoff: u64,
145 /// the maximum amount of milliseconds to wait before retrying
146 pub max_backoff: u64,
147}
148
149impl IntoIterator for RetryConfig {
150 type Item = Duration;
151 type IntoIter = std::iter::Take<delay::Range>;
152 fn into_iter(self) -> Self::IntoIter {
153 delay::Range::from_millis_inclusive(self.min_backoff, self.max_backoff).take(self.count)
154 }
155}
156
157#[derive(Debug)]
158pub enum OperationResult<T, E> {
159 /// Contains the success value.
160 Ok(T),
161 /// Contains the error value if duration is exceeded.
162 Retry(E),
163 /// Contains an error value to return immediately.
164 Err(E),
165}
166
167impl<T, E> From<Result<T, E>> for OperationResult<T, E> {
168 fn from(item: Result<T, E>) -> Self {
169 match item {
170 Ok(v) => OperationResult::Ok(v),
171 Err(e) => OperationResult::Retry(e),
172 }
173 }
174}
175
176/// Retry the given operation until it succeeds, or until the given `Duration`
177/// iterator ends.
178pub fn retry_fn<D, O, OR, R, E>(durations: D, mut operation: O) -> Result<R, E>
179where
180 D: IntoIterator<Item = Duration>,
181 O: FnMut() -> OR,
182 OR: Into<OperationResult<R, E>>,
183{
184 retry!(durations, { operation() })
185}