use std::future::Future;
use std::time::Duration;
use crate::error::{BooruError, Result};
pub const DEFAULT_MAX_RETRIES: u32 = 3;
pub const DEFAULT_INITIAL_DELAY_MS: u64 = 100;
pub const DEFAULT_MAX_DELAY_MS: u64 = 5000;
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub backoff_factor: f64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: DEFAULT_MAX_RETRIES,
initial_delay: Duration::from_millis(DEFAULT_INITIAL_DELAY_MS),
max_delay: Duration::from_millis(DEFAULT_MAX_DELAY_MS),
backoff_factor: 2.0,
}
}
}
impl RetryConfig {
#[must_use]
pub fn new(max_retries: u32) -> Self {
Self {
max_retries,
..Default::default()
}
}
#[must_use]
pub fn no_retry() -> Self {
Self {
max_retries: 0,
..Default::default()
}
}
#[must_use]
pub fn with_initial_delay(mut self, delay: Duration) -> Self {
self.initial_delay = delay;
self
}
#[must_use]
pub fn with_max_delay(mut self, delay: Duration) -> Self {
self.max_delay = delay;
self
}
#[must_use]
pub fn with_backoff_factor(mut self, factor: f64) -> Self {
self.backoff_factor = factor;
self
}
fn delay_for_attempt(&self, attempt: u32) -> Duration {
if attempt == 0 {
return Duration::ZERO;
}
let delay_ms = self.initial_delay.as_millis() as f64
* self.backoff_factor.powi(attempt.saturating_sub(1) as i32);
let delay = Duration::from_millis(delay_ms as u64);
delay.min(self.max_delay)
}
}
pub fn is_retryable(error: &BooruError) -> bool {
match error {
BooruError::Request(e) => {
if e.is_timeout() || e.is_connect() {
return true;
}
if let Some(status) = e.status() {
return status.is_server_error();
}
e.is_request()
}
BooruError::Parse(_) => false,
BooruError::TagLimitExceeded { .. } => false,
BooruError::PostNotFound(_) => false,
BooruError::EmptyResponse => false,
BooruError::InvalidUrl(_) => false,
BooruError::Unauthorized(_) => false,
BooruError::InvalidTag { .. } => false,
BooruError::RateLimited => true, BooruError::Io(_) => false, }
}
pub async fn with_retry<F, Fut, T>(config: RetryConfig, mut operation: F) -> Result<T>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T>>,
{
let mut attempt = 0;
let mut last_error;
loop {
match operation().await {
Ok(result) => return Ok(result),
Err(e) => {
last_error = e;
if attempt >= config.max_retries || !is_retryable(&last_error) {
return Err(last_error);
}
attempt += 1;
let delay = config.delay_for_attempt(attempt);
tokio::time::sleep(delay).await;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delay_calculation() {
let config = RetryConfig::default();
assert_eq!(config.delay_for_attempt(0), Duration::ZERO);
assert_eq!(config.delay_for_attempt(1), Duration::from_millis(100));
assert_eq!(config.delay_for_attempt(2), Duration::from_millis(200));
assert_eq!(config.delay_for_attempt(3), Duration::from_millis(400));
}
#[test]
fn test_delay_max_cap() {
let config = RetryConfig::default().with_max_delay(Duration::from_millis(150));
assert_eq!(config.delay_for_attempt(1), Duration::from_millis(100));
assert_eq!(config.delay_for_attempt(2), Duration::from_millis(150)); assert_eq!(config.delay_for_attempt(3), Duration::from_millis(150)); }
}