use std::env;
use std::time::Duration;
use time::OffsetDateTime;
#[derive(Clone, Copy, Debug)]
pub struct Limits {
pub base_spacing: Duration,
pub slow_spacing: Duration,
pub slowdown_duration: Duration,
pub base_cooldown: Duration,
pub max_cooldown: Duration,
pub jitter: Duration,
}
impl Limits {
pub fn test_fast(spacing_ms: u64, slow_spacing_ms: u64, cooldown_secs: u64) -> Self {
Self {
base_spacing: Duration::from_millis(spacing_ms),
slow_spacing: Duration::from_millis(slow_spacing_ms.max(spacing_ms)),
slowdown_duration: Duration::from_secs(cooldown_secs.saturating_mul(10)),
base_cooldown: Duration::from_secs(cooldown_secs.max(1)),
max_cooldown: Duration::from_secs(cooldown_secs.max(1).saturating_mul(5)),
jitter: Duration::from_millis(0),
}
}
pub fn from_env() -> Self {
let mut limits = Self::default();
if let Some(ms) = env_u64("DUCKDUCKGO_BASE_SPACING_MS").filter(|v| *v <= 60_000) {
limits.base_spacing = Duration::from_millis(ms);
}
if let Some(ms) = env_u64("DUCKDUCKGO_SLOW_SPACING_MS").filter(|v| *v <= 120_000) {
let candidate = Duration::from_millis(ms);
limits.slow_spacing = candidate.max(limits.base_spacing);
}
if let Some(secs) = env_u64("DUCKDUCKGO_BASE_COOLDOWN_S").filter(|v| (1..=600).contains(v))
{
limits.base_cooldown = Duration::from_secs(secs);
limits.max_cooldown = limits.max_cooldown.max(limits.base_cooldown);
}
limits
}
}
impl Default for Limits {
fn default() -> Self {
Self {
base_spacing: Duration::from_millis(2_000),
slow_spacing: Duration::from_millis(4_000),
slowdown_duration: Duration::from_secs(600),
base_cooldown: Duration::from_secs(60),
max_cooldown: Duration::from_secs(300),
jitter: Duration::from_millis(300),
}
}
}
impl Limits {
pub fn cooldown_for(&self, consecutive_blocks: u32) -> Duration {
let exp = consecutive_blocks.saturating_sub(1).min(8);
let scaled = self.base_cooldown.saturating_mul(2_u32.saturating_pow(exp));
scaled.min(self.max_cooldown)
}
pub fn spacing(&self, slowdown_until: Option<OffsetDateTime>, now: OffsetDateTime) -> Duration {
if slowdown_until.is_some_and(|t| t > now) {
self.slow_spacing
} else {
self.base_spacing
}
}
}
fn env_u64(key: &str) -> Option<u64> {
env::var(key).ok().and_then(|v| v.parse::<u64>().ok())
}
#[cfg(test)]
mod tests {
use super::Limits;
use std::time::Duration;
#[test]
fn cooldown_doubles_then_caps() {
let limits = Limits::default();
assert_eq!(limits.cooldown_for(0), Duration::from_secs(60));
assert_eq!(limits.cooldown_for(1), Duration::from_secs(60));
assert_eq!(limits.cooldown_for(2), Duration::from_secs(120));
assert_eq!(limits.cooldown_for(3), Duration::from_secs(240));
assert_eq!(limits.cooldown_for(4), Duration::from_secs(300));
assert_eq!(limits.cooldown_for(8), Duration::from_secs(300));
assert_eq!(limits.cooldown_for(99), Duration::from_secs(300));
}
#[test]
fn spacing_picks_slow_inside_window() {
let limits = Limits::default();
let now = time::OffsetDateTime::now_utc();
let in_future = now + Duration::from_secs(60);
let in_past = now - Duration::from_secs(60);
assert_eq!(
limits.spacing(Some(in_future), now),
Duration::from_millis(4_000)
);
assert_eq!(
limits.spacing(Some(in_past), now),
Duration::from_millis(2_000)
);
assert_eq!(limits.spacing(None, now), Duration::from_millis(2_000));
}
}