use crate::errors::AniListError;
use std::time::Duration;
use tokio::time::sleep;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryConfig {
pub max_retries: u32,
pub base_delay_ms: u64,
pub exponential_backoff: bool,
pub max_delay_ms: u64,
pub use_jitter: bool,
pub retry_on_server_error: bool,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
base_delay_ms: 1000,
exponential_backoff: true,
max_delay_ms: 30000,
use_jitter: false,
retry_on_server_error: false,
}
}
}
impl RetryConfig {
#[must_use]
pub fn for_rate_limits() -> Self {
Self {
max_retries: 5,
base_delay_ms: 60000, exponential_backoff: false, max_delay_ms: 120000, use_jitter: false,
retry_on_server_error: false,
}
}
#[must_use]
pub fn aggressive() -> Self {
Self {
max_retries: 5,
base_delay_ms: 500,
exponential_backoff: true,
max_delay_ms: 10000,
use_jitter: false,
retry_on_server_error: false,
}
}
#[inline]
fn calculate_delay(&self, attempts: u32) -> Duration {
let delay = if self.exponential_backoff {
self.base_delay_ms.saturating_mul(1u64 << attempts.min(10))
} else {
self.base_delay_ms
};
let duration = Duration::from_millis(delay.min(self.max_delay_ms));
if self.use_jitter {
add_jitter(duration)
} else {
duration
}
}
}
pub async fn retry_with_backoff<F, Fut, T>(
mut operation: F,
config: RetryConfig,
) -> Result<T, AniListError>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T, AniListError>>,
{
let mut attempts = 0u32;
loop {
match operation().await {
Ok(result) => return Ok(result),
Err(err) => {
let (should_retry, sleep_duration) = match &err {
AniListError::RateLimit { retry_after, .. } => {
if attempts >= config.max_retries {
return Err(err);
}
let duration = if *retry_after > 0 && *retry_after <= 300 {
let d = Duration::from_secs(*retry_after as u64);
if config.use_jitter { add_jitter(d) } else { d }
} else {
config.calculate_delay(attempts)
};
(true, duration)
}
AniListError::RateLimitSimple => {
if attempts >= config.max_retries {
return Err(err);
}
(true, config.calculate_delay(attempts))
}
AniListError::BurstLimit => {
if attempts >= config.max_retries {
return Err(err);
}
let duration = config.calculate_delay(attempts + 1);
(true, duration)
}
AniListError::ServerError { .. }
if config.retry_on_server_error && attempts < config.max_retries =>
{
(true, config.calculate_delay(attempts))
}
_ => return Err(err),
};
if should_retry {
log::warn!(
"Rate limited. Retrying in {} seconds... (attempt {}/{})",
sleep_duration.as_secs(),
attempts + 1,
config.max_retries
);
crate::trace_warn!(
attempt = attempts + 1,
max_retries = config.max_retries,
delay_secs = sleep_duration.as_secs(),
"Rate limited. Retrying..."
);
sleep(sleep_duration).await;
attempts += 1;
}
}
}
}
}
#[inline]
pub fn add_jitter(duration: Duration) -> Duration {
use std::time::SystemTime;
let seed = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(1337) as u64;
let a = 6364136223846793005u64;
let c = 1442695040888963407u64;
let rand_val = seed.wrapping_mul(a).wrapping_add(c);
let duration_ms = duration.as_millis() as u64;
let jittered_ms = duration_ms.saturating_mul(80 + (rand_val % 41)) / 100;
Duration::from_millis(jittered_ms.max(1))
}
#[inline]
pub async fn rate_limit_delay(delay_ms: u64) {
sleep(Duration::from_millis(delay_ms)).await;
}
#[inline]
pub fn calculate_delay(remaining: u32, reset_in_seconds: u64) -> Duration {
match remaining {
0 => Duration::from_secs(reset_in_seconds),
1..=9 => Duration::from_millis(2000), 10..=29 => Duration::from_millis(1000), _ => Duration::from_millis(500), }
}
#[cfg(feature = "tracing")]
#[macro_export]
macro_rules! trace_span {
($name:expr) => {
let _span = tracing::info_span!($name).entered();
};
($name:expr, $($fields:tt)*) => {
let _span = tracing::info_span!($name, $($fields)*).entered();
};
}
#[cfg(not(feature = "tracing"))]
#[macro_export]
macro_rules! trace_span {
($name:expr) => {};
($name:expr, $($fields:tt)*) => {};
}
#[cfg(feature = "tracing")]
#[macro_export]
macro_rules! trace_info {
($($arg:tt)*) => {
tracing::info!($($arg)*);
};
}
#[cfg(not(feature = "tracing"))]
#[macro_export]
macro_rules! trace_info {
($($arg:tt)*) => {};
}
#[cfg(feature = "tracing")]
#[macro_export]
macro_rules! trace_warn {
($($arg:tt)*) => {
tracing::warn!($($arg)*);
};
}
#[cfg(not(feature = "tracing"))]
#[macro_export]
macro_rules! trace_warn {
($($arg:tt)*) => {};
}
#[cfg(feature = "tracing")]
#[macro_export]
macro_rules! trace_error {
($($arg:tt)*) => {
tracing::error!($($arg)*);
};
}
#[cfg(not(feature = "tracing"))]
#[macro_export]
macro_rules! trace_error {
($($arg:tt)*) => {};
}