use std::time::Duration;
pub(super) const FALLBACK_THROTTLE_SECS: u64 = 60;
#[allow(clippy::redundant_pub_crate, reason = "see comment above")]
pub(crate) const THROTTLE_DECAY_GRACE_SECS: i64 = 120;
#[must_use]
pub(super) fn throttle_delay(
throttle_attempts: i32,
enabled: bool,
base_seconds: i32,
max_seconds: i32,
) -> Duration {
if !enabled {
return Duration::from_secs(FALLBACK_THROTTLE_SECS);
}
let base = u64::try_from(base_seconds.max(1)).unwrap_or(1);
let max = u64::try_from(max_seconds.max(1)).unwrap_or(FALLBACK_THROTTLE_SECS);
let exp = u32::try_from(throttle_attempts.max(0)).unwrap_or(0).min(30);
let secs = base.saturating_mul(1u64 << exp).min(max);
Duration::from_secs(secs)
}
#[allow(clippy::redundant_pub_crate, reason = "see comment above")]
#[must_use]
pub(crate) fn failed_delay(
attempts: i32,
enabled: bool,
base_seconds: i32,
max_seconds: i32,
) -> Duration {
if !enabled {
return Duration::ZERO;
}
let base = u64::try_from(base_seconds.max(1)).unwrap_or(1);
let max = u64::try_from(max_seconds.max(1)).unwrap_or(FALLBACK_THROTTLE_SECS);
let exp = u32::try_from(attempts.max(0)).unwrap_or(0).min(30);
let secs = base.saturating_mul(1u64 << exp).min(max);
Duration::from_secs(secs)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn throttle_disabled_returns_flat_fallback() {
let d = throttle_delay(0, false, 60, 1800);
assert_eq!(d.as_secs(), FALLBACK_THROTTLE_SECS);
let d = throttle_delay(20, false, 60, 1800);
assert_eq!(d.as_secs(), FALLBACK_THROTTLE_SECS);
}
#[test]
fn throttle_enabled_grows_exponentially() {
assert_eq!(throttle_delay(0, true, 60, 1800).as_secs(), 60);
assert_eq!(throttle_delay(1, true, 60, 1800).as_secs(), 120);
assert_eq!(throttle_delay(2, true, 60, 1800).as_secs(), 240);
assert_eq!(throttle_delay(5, true, 60, 1800).as_secs(), 1800);
}
#[test]
fn throttle_enabled_caps_at_max() {
assert_eq!(throttle_delay(30, true, 60, 1800).as_secs(), 1800);
assert_eq!(throttle_delay(100, true, 60, 1800).as_secs(), 1800);
}
#[test]
fn failed_disabled_returns_zero() {
assert_eq!(failed_delay(0, false, 60, 1800), Duration::ZERO);
assert_eq!(failed_delay(1, false, 60, 1800), Duration::ZERO);
assert_eq!(failed_delay(20, false, 60, 1800), Duration::ZERO);
assert_eq!(failed_delay(5, false, 0, 0), Duration::ZERO);
}
#[test]
fn failed_enabled_grows_exponentially() {
assert_eq!(failed_delay(0, true, 60, 1800).as_secs(), 60);
assert_eq!(failed_delay(1, true, 60, 1800).as_secs(), 120);
assert_eq!(failed_delay(2, true, 60, 1800).as_secs(), 240);
assert_eq!(failed_delay(5, true, 60, 1800).as_secs(), 1800);
}
#[test]
fn failed_enabled_caps_at_max() {
assert_eq!(failed_delay(30, true, 60, 1800).as_secs(), 1800);
assert_eq!(failed_delay(100, true, 60, 1800).as_secs(), 1800);
}
#[test]
fn failed_enabled_honours_custom_base_and_max() {
assert_eq!(failed_delay(0, true, 10, 300).as_secs(), 10);
assert_eq!(failed_delay(3, true, 10, 300).as_secs(), 80);
assert_eq!(failed_delay(10, true, 10, 300).as_secs(), 300);
}
#[test]
fn failed_negative_attempts_clamp_to_zero() {
assert_eq!(failed_delay(-5, true, 60, 1800).as_secs(), 60);
}
}