1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Crate retry provides a function, `retry`, which runs an operation continuously until its
//! return value satisfies a specific condition. The maximum number of times to try and the amount
//! of time to wait between tries is configurable.

use std::error::Error;
use std::fmt::{Display,Formatter};
use std::fmt::Error as FmtError;
use std::thread::sleep_ms;

/// An error indicating that a retry call failed.
#[derive(Debug)]
pub struct RetryError {
    message: &'static str
}

impl Display for RetryError {
    fn fmt(&self, formatter: &mut Formatter) -> Result<(), FmtError> {
        write!(formatter, "{}", self.message)
    }
}

impl Error for RetryError {
    fn description(&self) -> &str {
        self.message
    }
}

/// Invokes a function until a condition is satisfied.
///
/// `value_fn` is a closure that will be executed to produce a value. `condition_fn` is a closure
/// that takes the value produced by `value_fn` and returns a boolean indicating whether or not
/// some desired condition is true. `retry` will invoke `value_fn` up to `tries` times, returning
/// the value as soon as `condition_fn` is satisfied, or returning an error when the last try was
/// unsuccessful. It will wait `wait` milliseconds after each unsuccessful try.
///
/// # Failures
///
/// Will fail when:
///
/// 1. `value_fn` has been been invoked `tries` times and has still not satisfied `condition_fn`.
/// 2. `tries` is zero. It must be at least 1.
///
/// # Examples
///
/// Imagine an HTTP API with an endpoint that returns 204 No Content while a job is processing, and
/// eventually 200 OK when the job has completed. Retrying until the job is finished would be
/// written:
///
/// ```
/// # use retry::retry;
/// # struct Client;
/// # impl Client {
/// #     fn query_job_status(&self) -> Response {
/// #         Response {
/// #             code: 200,
/// #             body: "success",
/// #         }
/// #     }
/// # }
/// # struct Response {
/// #     code: u16,
/// #     body: &'static str,
/// # }
/// # let api_client = Client;
/// match retry(10, 500, || api_client.query_job_status(), |response| response.code == 200) {
///     Ok(response) => println!("Job completed with result: {}", response.body),
///     Err(error) => println!("Job completion could not be verified: {}", error),
/// }
/// ```
///
/// This retries the API call up to 10 times, waiting 500 milliseconds after each unsuccesful
/// attempt.
pub fn retry<F, G, R>(
    tries: usize,
    wait: u32,
    mut value_fn: F,
    mut condition_fn: G
) -> Result<R, RetryError> where F: FnMut() -> R, G: FnMut(&R) -> bool {
    if tries == 0 {
        return Err(RetryError { message: "tries must be non-zero" });
    }

    for _ in 0..tries {
        let value = value_fn();

        if condition_fn(&value) {
            return Ok(value);
        }

        sleep_ms(wait);
    }

    Err(RetryError { message: "reached last try without condition match" })
}

#[cfg(test)]
mod tests {
    use super::retry;

    #[test]
    fn succeeds_on_first_try() {
        let value = retry(1, 0, || 1, |value| *value == 1).ok().unwrap();

        assert_eq!(value, 1);
    }

    #[test]
    fn requires_non_zero_tries() {
        let error = retry(0, 0, || 1, |value| *value == 1).err().unwrap();

        assert_eq!(error.message, "tries must be non-zero");
    }

    #[test]
    fn succeeds_on_subsequent_try() {
        let mut collection = vec![1, 2].into_iter();

        let value = retry(2, 0, || collection.next().unwrap(), |value| *value == 2).ok().unwrap();

        assert_eq!(value, 2);
    }

    #[test]
    fn fails_after_last_try() {
        let mut collection = vec![1].into_iter();

        let error = retry(1, 0, || collection.next().unwrap(), |value| *value == 2).err().unwrap();

        assert_eq!(error.message, "reached last try without condition match");
    }
}