use std::time::Duration;
use libdd_capabilities::sleep::SleepCapability;
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub enum RetryBackoffType {
Linear,
Constant,
Exponential,
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub struct RetryStrategy {
max_retries: u32,
delay_ms: Duration,
backoff_type: RetryBackoffType,
jitter: Option<Duration>,
}
impl Default for RetryStrategy {
fn default() -> Self {
RetryStrategy {
max_retries: 5,
delay_ms: Duration::from_millis(100),
backoff_type: RetryBackoffType::Exponential,
jitter: None,
}
}
}
impl RetryStrategy {
pub fn new(
max_retries: u32,
delay_ms: u64,
backoff_type: RetryBackoffType,
jitter: Option<u64>,
) -> RetryStrategy {
RetryStrategy {
max_retries,
delay_ms: Duration::from_millis(delay_ms),
backoff_type,
jitter: jitter.map(Duration::from_millis),
}
}
pub(crate) async fn delay<C: SleepCapability>(&self, attempt: u32, capabilities: &C) {
let delay = match self.backoff_type {
RetryBackoffType::Exponential => self.delay_ms * 2u32.pow(attempt - 1),
RetryBackoffType::Constant => self.delay_ms,
RetryBackoffType::Linear => self.delay_ms + (self.delay_ms * (attempt - 1)),
};
if let Some(jitter) = self.jitter {
let jitter = rand::random::<u64>() % jitter.as_millis() as u64;
capabilities
.sleep(delay + Duration::from_millis(jitter))
.await;
} else {
capabilities.sleep(delay).await;
}
}
pub(crate) fn max_retries(&self) -> u32 {
self.max_retries
}
}
#[cfg(test)]
mod tests {
use super::*;
use libdd_capabilities_impl::NativeSleepCapability;
use tokio::time::Instant;
const RETRY_STRATEGY_TIME_TOLERANCE_MS: u64 = 100;
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn test_retry_strategy_constant() {
let retry_strategy = RetryStrategy {
max_retries: 5,
delay_ms: Duration::from_millis(100),
backoff_type: RetryBackoffType::Constant,
jitter: None,
};
let capabilities = NativeSleepCapability;
let start = Instant::now();
retry_strategy.delay(1, &capabilities).await;
let elapsed = start.elapsed();
assert!(
elapsed >= retry_strategy.delay_ms
&& elapsed
<= retry_strategy.delay_ms
+ Duration::from_millis(RETRY_STRATEGY_TIME_TOLERANCE_MS),
"Elapsed time of {} ms was not within expected range",
elapsed.as_millis()
);
let start = Instant::now();
retry_strategy.delay(2, &capabilities).await;
let elapsed = start.elapsed();
assert!(
elapsed >= retry_strategy.delay_ms
&& elapsed
<= retry_strategy.delay_ms
+ Duration::from_millis(RETRY_STRATEGY_TIME_TOLERANCE_MS),
"Elapsed time of {} ms was not within expected range",
elapsed.as_millis()
);
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn test_retry_strategy_linear() {
let retry_strategy = RetryStrategy {
max_retries: 5,
delay_ms: Duration::from_millis(100),
backoff_type: RetryBackoffType::Linear,
jitter: None,
};
let capabilities = NativeSleepCapability;
let start = Instant::now();
retry_strategy.delay(1, &capabilities).await;
let elapsed = start.elapsed();
assert!(
elapsed >= retry_strategy.delay_ms
&& elapsed
<= retry_strategy.delay_ms
+ Duration::from_millis(RETRY_STRATEGY_TIME_TOLERANCE_MS),
"Elapsed time of {} ms was not within expected range",
elapsed.as_millis()
);
let start = Instant::now();
retry_strategy.delay(3, &capabilities).await;
let elapsed = start.elapsed();
assert!(
elapsed >= retry_strategy.delay_ms + (retry_strategy.delay_ms * 2)
&& elapsed
<= retry_strategy.delay_ms
+ (retry_strategy.delay_ms * 2)
+ Duration::from_millis(RETRY_STRATEGY_TIME_TOLERANCE_MS),
"Elapsed time of {} ms was not within expected range",
elapsed.as_millis()
);
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn test_retry_strategy_exponential() {
let retry_strategy = RetryStrategy {
max_retries: 5,
delay_ms: Duration::from_millis(100),
backoff_type: RetryBackoffType::Exponential,
jitter: None,
};
let capabilities = NativeSleepCapability;
let start = Instant::now();
retry_strategy.delay(1, &capabilities).await;
let elapsed = start.elapsed();
assert!(
elapsed >= retry_strategy.delay_ms
&& elapsed
<= retry_strategy.delay_ms
+ Duration::from_millis(RETRY_STRATEGY_TIME_TOLERANCE_MS),
"Elapsed time of {} ms was not within expected range",
elapsed.as_millis()
);
let start = Instant::now();
retry_strategy.delay(3, &capabilities).await;
let elapsed = start.elapsed();
assert!(
elapsed >= retry_strategy.delay_ms * 4
&& elapsed
<= retry_strategy.delay_ms * 4
+ Duration::from_millis(RETRY_STRATEGY_TIME_TOLERANCE_MS),
"Elapsed time of {} ms was not within expected range",
elapsed.as_millis()
);
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn test_retry_strategy_jitter() {
let retry_strategy = RetryStrategy {
max_retries: 5,
delay_ms: Duration::from_millis(100),
backoff_type: RetryBackoffType::Constant,
jitter: Some(Duration::from_millis(50)),
};
let capabilities = NativeSleepCapability;
let start = Instant::now();
retry_strategy.delay(1, &capabilities).await;
let elapsed = start.elapsed();
assert!(
elapsed >= retry_strategy.delay_ms
&& elapsed
<= retry_strategy.delay_ms
+ retry_strategy.jitter.unwrap()
+ Duration::from_millis(RETRY_STRATEGY_TIME_TOLERANCE_MS),
"Elapsed time of {} ms was not within expected range",
elapsed.as_millis()
);
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn test_retry_strategy_max_retries() {
let retry_strategy = RetryStrategy {
max_retries: 17,
delay_ms: Duration::from_millis(100),
backoff_type: RetryBackoffType::Constant,
jitter: Some(Duration::from_millis(50)),
};
assert_eq!(
retry_strategy.max_retries(),
17,
"Max retries did not match expected value"
);
}
}