use std::sync::Arc;
use std::time::Duration;
use crate::api::FailureDetails;
pub type RetryHandle = Arc<dyn Fn(&FailureDetails) -> bool + Send + Sync>;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct RetryPolicy {
pub max_number_of_attempts: u32,
pub first_retry_interval: Duration,
pub backoff_coefficient: f64,
pub max_retry_interval: Option<Duration>,
pub retry_timeout: Option<Duration>,
#[serde(skip)]
pub handle: Option<RetryHandle>,
}
impl std::fmt::Debug for RetryPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RetryPolicy")
.field("max_number_of_attempts", &self.max_number_of_attempts)
.field("first_retry_interval", &self.first_retry_interval)
.field("backoff_coefficient", &self.backoff_coefficient)
.field("max_retry_interval", &self.max_retry_interval)
.field("retry_timeout", &self.retry_timeout)
.field("handle", &self.handle.as_ref().map(|_| "<fn>"))
.finish()
}
}
impl RetryPolicy {
pub fn new(max_number_of_attempts: u32, first_retry_interval: Duration) -> Self {
Self {
max_number_of_attempts,
first_retry_interval,
backoff_coefficient: 1.0,
max_retry_interval: None,
retry_timeout: None,
handle: None,
}
}
pub fn with_backoff_coefficient(mut self, coefficient: f64) -> Self {
self.backoff_coefficient = coefficient;
self
}
pub fn with_max_retry_interval(mut self, interval: Duration) -> Self {
self.max_retry_interval = Some(interval);
self
}
pub fn with_retry_timeout(mut self, timeout: Duration) -> Self {
self.retry_timeout = Some(timeout);
self
}
pub fn with_handle<F>(mut self, f: F) -> Self
where
F: Fn(&FailureDetails) -> bool + Send + Sync + 'static,
{
self.handle = Some(Arc::new(f));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_sets_defaults() {
let p = RetryPolicy::new(5, Duration::from_secs(2));
assert_eq!(p.max_number_of_attempts, 5);
assert_eq!(p.first_retry_interval, Duration::from_secs(2));
assert!((p.backoff_coefficient - 1.0).abs() < f64::EPSILON);
assert!(p.max_retry_interval.is_none());
assert!(p.retry_timeout.is_none());
assert!(p.handle.is_none());
}
#[test]
fn with_backoff_coefficient() {
let p = RetryPolicy::new(3, Duration::from_secs(1)).with_backoff_coefficient(2.5);
assert!((p.backoff_coefficient - 2.5).abs() < f64::EPSILON);
}
#[test]
fn with_max_retry_interval() {
let p = RetryPolicy::new(3, Duration::from_secs(1))
.with_max_retry_interval(Duration::from_secs(60));
assert_eq!(p.max_retry_interval, Some(Duration::from_secs(60)));
}
#[test]
fn with_retry_timeout() {
let p = RetryPolicy::new(3, Duration::from_secs(1))
.with_retry_timeout(Duration::from_secs(300));
assert_eq!(p.retry_timeout, Some(Duration::from_secs(300)));
}
#[test]
fn with_handle_is_callable() {
let p = RetryPolicy::new(3, Duration::from_secs(1)).with_handle(|_details| true);
let fd = FailureDetails {
message: "err".into(),
error_type: "E".into(),
stack_trace: None,
};
assert!((p.handle.unwrap())(&fd));
}
#[test]
fn debug_with_handle() {
let p = RetryPolicy::new(1, Duration::from_secs(1)).with_handle(|_| false);
let dbg = format!("{p:?}");
assert!(dbg.contains(r#"Some("<fn>")"#));
}
#[test]
fn debug_without_handle() {
let p = RetryPolicy::new(1, Duration::from_secs(1));
let dbg = format!("{p:?}");
assert!(dbg.contains("handle: None"));
}
}