Skip to content

Commit 100c162

Browse files
committed
Fix #19: Permit exiting early without more retries
This is important for many cases where only some cases are safe to retry (e.g. non idempotent HTTP operations), or file system operations where unknown errors are undefined behaviour. Signed-off-by: Robert Collins <[email protected]>
1 parent 1dfb004 commit 100c162

File tree

2 files changed

+157
-34
lines changed

2 files changed

+157
-34
lines changed

src/lib.rs

Lines changed: 108 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@
22
//!
33
//! # Usage
44
//!
5-
//! Retry an operation using the `retry` function. `retry` accepts an iterator over `Duration`s and
6-
//! a closure that returns a `Result`. The iterator is used to determine how long to wait after
7-
//! each unsuccessful try and how many times to try before giving up and returning `Result::Err`.
5+
//! Retry an operation using the `retry` function. `retry` accepts an iterator
6+
//! over `Duration`s and a closure that returns a `retry::OperationResult`. The
7+
//! iterator is used to determine how long to wait after each unsuccessful try
8+
//! and how many times to try before giving up and returning `Result::Err`. The
9+
//! closure is used to determine either the value to return, or whether to retry
10+
//! on non-fatal errors, or to immediately stop on fatal errors.
811
//!
912
//! Any type that implements `Iterator<Duration>` can be used to determine retry behavior, though a
1013
//! few useful implementations are provided in the `delay` module, including a fixed delay and
1114
//! exponential back-off.
1215
//!
16+
//! `retry::OperationResult` implements `From` for `std::result::Result`, so for
17+
//! the reasonably common case where no fatal errors are expected, just use
18+
//! `.into()` to cast into the required type - `Result::Err` becomes
19+
//! `OperationResult::Retry`.
20+
//!
1321
//! ```
1422
//! # use retry::retry;
1523
//! # use retry::delay::Fixed;
@@ -20,7 +28,7 @@
2028
//! Some(n) if n == 3 => Ok("n is 3!"),
2129
//! Some(_) => Err("n must be 3!"),
2230
//! None => Err("n was never 3!"),
23-
//! }
31+
//! }.into()
2432
//! });
2533
//!
2634
//! assert!(result.is_ok());
@@ -39,7 +47,7 @@
3947
//! Some(n) if n == 3 => Ok("n is 3!"),
4048
//! Some(_) => Err("n must be 3!"),
4149
//! None => Err("n was never 3!"),
42-
//! }
50+
//! }.into()
4351
//! });
4452
//!
4553
//! assert!(result.is_err());
@@ -58,11 +66,29 @@
5866
//! Some(n) if n == 3 => Ok("n is 3!"),
5967
//! Some(_) => Err("n must be 3!"),
6068
//! None => Err("n was never 3!"),
61-
//! }
69+
//! }.into()
6270
//! });
6371
//!
6472
//! assert!(result.is_ok());
6573
//! ```
74+
//!
75+
//! To deal with fatal errors, return `OperationResult` directlythread::spawn(move || {
76+
//!
77+
//! ```
78+
//! # use retry::retry;
79+
//! # use retry::delay::Fixed;
80+
//! use retry::OperationResult;
81+
//! let mut collection = vec![1, 2].into_iter();
82+
//! let value = retry(Fixed::from_millis(1), || {
83+
//! match collection.next() {
84+
//! Some(n) if n == 2 => OperationResult::Ok(n),
85+
//! Some(_) => OperationResult::Retry("not 2"),
86+
//! None => OperationResult::Err("not found"),
87+
//! }
88+
//! }).unwrap();
89+
//!
90+
//! assert_eq!(value, 2);
91+
//! ```
6692
6793
#![deny(missing_debug_implementations, missing_docs, warnings)]
6894

@@ -74,22 +100,24 @@ use std::{
74100
};
75101

76102
pub mod delay;
103+
pub mod opresult;
104+
105+
pub use opresult::OperationResult;
77106

78107
/// Retry the given operation synchronously until it succeeds, or until the given `Duration`
79-
/// iterator ends.
80108
pub fn retry<I, O, R, E>(iterable: I, mut operation: O) -> Result<R, Error<E>>
81109
where
82110
I: IntoIterator<Item = Duration>,
83-
O: FnMut() -> Result<R, E>,
111+
O: FnMut() -> OperationResult<R, E>,
84112
{
85113
let mut iterator = iterable.into_iter();
86114
let mut current_try = 1;
87115
let mut total_delay = Duration::default();
88116

89117
loop {
90118
match operation() {
91-
Ok(value) => return Ok(value),
92-
Err(error) => {
119+
OperationResult::Ok(value) => return Ok(value),
120+
OperationResult::Retry(error) => {
93121
if let Some(delay) = iterator.next() {
94122
sleep(delay);
95123
current_try += 1;
@@ -102,6 +130,13 @@ where
102130
});
103131
}
104132
}
133+
OperationResult::Err(error) => {
134+
return Err(Error::Operation {
135+
error: error,
136+
total_delay: total_delay,
137+
tries: current_try,
138+
});
139+
}
105140
}
106141
}
107142
}
@@ -158,16 +193,20 @@ mod tests {
158193
use std::time::Duration;
159194

160195
use super::delay::{Exponential, Fixed, NoDelay, Range};
196+
use super::opresult::OperationResult;
161197
use super::{retry, Error};
162198

163199
#[test]
164200
fn succeeds_with_infinite_retries() {
165201
let mut collection = vec![1, 2, 3, 4, 5].into_iter();
166202

167-
let value = retry(NoDelay, || match collection.next() {
168-
Some(n) if n == 5 => Ok(n),
169-
Some(_) => Err("not 5"),
170-
None => Err("not 5"),
203+
let value = retry(NoDelay, || {
204+
match collection.next() {
205+
Some(n) if n == 5 => Ok(n),
206+
Some(_) => Err("not 5"),
207+
None => Err("not 5"),
208+
}
209+
.into()
171210
})
172211
.unwrap();
173212

@@ -178,10 +217,13 @@ mod tests {
178217
fn succeeds_with_maximum_retries() {
179218
let mut collection = vec![1, 2].into_iter();
180219

181-
let value = retry(NoDelay.take(1), || match collection.next() {
182-
Some(n) if n == 2 => Ok(n),
183-
Some(_) => Err("not 2"),
184-
None => Err("not 2"),
220+
let value = retry(NoDelay.take(1), || {
221+
match collection.next() {
222+
Some(n) if n == 2 => Ok(n),
223+
Some(_) => Err("not 2"),
224+
None => Err("not 2"),
225+
}
226+
.into()
185227
})
186228
.unwrap();
187229

@@ -192,10 +234,13 @@ mod tests {
192234
fn fails_after_last_try() {
193235
let mut collection = vec![1].into_iter();
194236

195-
let res = retry(NoDelay.take(1), || match collection.next() {
196-
Some(n) if n == 2 => Ok(n),
197-
Some(_) => Err("not 2"),
198-
None => Err("not 2"),
237+
let res = retry(NoDelay.take(1), || {
238+
match collection.next() {
239+
Some(n) if n == 2 => Ok(n),
240+
Some(_) => Err("not 2"),
241+
None => Err("not 2"),
242+
}
243+
.into()
199244
});
200245

201246
assert_eq!(
@@ -208,14 +253,37 @@ mod tests {
208253
);
209254
}
210255

256+
#[test]
257+
fn fatal_errors() {
258+
let mut collection = vec![1].into_iter();
259+
260+
let res = retry(NoDelay.take(2), || match collection.next() {
261+
Some(n) if n == 2 => OperationResult::Ok(n),
262+
Some(_) => OperationResult::Err("no retry"),
263+
None => OperationResult::Err("not 2"),
264+
});
265+
266+
assert_eq!(
267+
res,
268+
Err(Error::Operation {
269+
error: "no retry",
270+
tries: 1,
271+
total_delay: Duration::from_millis(0)
272+
})
273+
);
274+
}
275+
211276
#[test]
212277
fn succeeds_with_fixed_delay() {
213278
let mut collection = vec![1, 2].into_iter();
214279

215-
let value = retry(Fixed::from_millis(1), || match collection.next() {
216-
Some(n) if n == 2 => Ok(n),
217-
Some(_) => Err("not 2"),
218-
None => Err("not 2"),
280+
let value = retry(Fixed::from_millis(1), || {
281+
match collection.next() {
282+
Some(n) if n == 2 => Ok(n),
283+
Some(_) => Err("not 2"),
284+
None => Err("not 2"),
285+
}
286+
.into()
219287
})
220288
.unwrap();
221289

@@ -226,10 +294,13 @@ mod tests {
226294
fn succeeds_with_exponential_delay() {
227295
let mut collection = vec![1, 2].into_iter();
228296

229-
let value = retry(Exponential::from_millis(1), || match collection.next() {
230-
Some(n) if n == 2 => Ok(n),
231-
Some(_) => Err("not 2"),
232-
None => Err("not 2"),
297+
let value = retry(Exponential::from_millis(1), || {
298+
match collection.next() {
299+
Some(n) if n == 2 => Ok(n),
300+
Some(_) => Err("not 2"),
301+
None => Err("not 2"),
302+
}
303+
.into()
233304
})
234305
.unwrap();
235306

@@ -240,10 +311,13 @@ mod tests {
240311
fn succeeds_with_ranged_delay() {
241312
let mut collection = vec![1, 2].into_iter();
242313

243-
let value = retry(Range::from_millis_exclusive(1, 10), || match collection.next() {
244-
Some(n) if n == 2 => Ok(n),
245-
Some(_) => Err("not 2"),
246-
None => Err("not 2"),
314+
let value = retry(Range::from_millis_exclusive(1, 10), || {
315+
match collection.next() {
316+
Some(n) if n == 2 => Ok(n),
317+
Some(_) => Err("not 2"),
318+
None => Err("not 2"),
319+
}
320+
.into()
247321
})
248322
.unwrap();
249323

src/opresult.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//! Provides a ternary result for operations
2+
//!
3+
//! # Examples
4+
//!
5+
//! ```rust
6+
//! # use retry::retry;
7+
//! # use retry::delay::Fixed;
8+
//! use retry::OperationResult;
9+
//! let mut collection = vec![1, 2].into_iter();
10+
//! let value = retry(Fixed::from_millis(1), || {
11+
//! match collection.next() {
12+
//! Some(n) if n == 2 => OperationResult::Ok(n),
13+
//! Some(_) => OperationResult::Retry("not 2"),
14+
//! None => OperationResult::Err("not found"),
15+
//! }
16+
//! }).unwrap();
17+
//!
18+
//! assert_eq!(value, 2);
19+
//! ```
20+
21+
/// `OperationResult` is a type that represents either success ([`Ok`]) or
22+
/// failure ([`Err`]) or possible failure that should be retried ([`Retry`]).
23+
#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
24+
pub enum OperationResult<T, E> {
25+
/// Contains the success value
26+
Ok(T),
27+
/// Contains the error value if duration is exceeded
28+
Retry(E),
29+
/// Contains an immediate error value
30+
Err(E),
31+
}
32+
33+
impl<T, E> From<Result<T, E>> for OperationResult<T, E> {
34+
fn from(item: Result<T, E>) -> Self {
35+
match item {
36+
Ok(v) => OperationResult::Ok(v),
37+
Err(e) => OperationResult::Retry(e),
38+
}
39+
}
40+
}
41+
42+
#[cfg(test)]
43+
mod tests {
44+
// use std::time::Duration;
45+
46+
// use super::{Error, retry};
47+
// use super::delay::{Exponential, Fixed, NoDelay, Range};
48+
// use super::opresult::OperationResult;
49+
}

0 commit comments

Comments
 (0)