use chrono::{DateTime, NaiveTime, Timelike, Utc};
use chrono_tz::Tz;
use std::time::Duration;
pub fn backoff_interval(base: Duration, consecutive_failures: u32) -> Duration {
let exponent = consecutive_failures.min(3); base * (1u32 << exponent)
}
pub fn check_active_hours(
active_hours: Option<(NaiveTime, NaiveTime)>,
tz: Tz,
) -> Option<Duration> {
let (window_start, window_end) = active_hours?;
let now_tz = Utc::now().with_timezone(&tz);
let now_time = now_tz.time();
let inside = if window_start <= window_end {
now_time >= window_start && now_time < window_end
} else {
now_time >= window_start || now_time < window_end
};
if inside {
return None; }
let secs_until = if now_time < window_start {
let delta = window_start - now_time;
delta.num_seconds()
} else {
let secs_left_today =
86_400i64 - now_time.num_seconds_from_midnight() as i64;
let secs_from_midnight = window_start.num_seconds_from_midnight() as i64;
secs_left_today + secs_from_midnight
};
let secs_until = secs_until.max(0) as u64;
Some(Duration::from_secs(secs_until))
}
pub fn startup_delay(interval: Duration, last_run_at: Option<DateTime<Utc>>) -> Duration {
const WARMUP: Duration = Duration::from_secs(30);
let Some(last) = last_run_at else {
return WARMUP;
};
let elapsed = Utc::now()
.signed_duration_since(last)
.to_std()
.unwrap_or(Duration::ZERO);
if elapsed < interval {
interval - elapsed
} else {
WARMUP
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration as CDuration;
#[test]
fn backoff_no_failures() {
let base = Duration::from_secs(60);
assert_eq!(backoff_interval(base, 0), Duration::from_secs(60));
}
#[test]
fn backoff_one_failure_doubles() {
let base = Duration::from_secs(60);
assert_eq!(backoff_interval(base, 1), Duration::from_secs(120));
}
#[test]
fn backoff_caps_at_8x() {
let base = Duration::from_secs(60);
assert_eq!(backoff_interval(base, 3), Duration::from_secs(480));
assert_eq!(backoff_interval(base, 10), Duration::from_secs(480));
}
#[test]
fn active_hours_none_always_active() {
let result = check_active_hours(None, chrono_tz::UTC);
assert!(result.is_none());
}
#[test]
fn startup_delay_no_last_run() {
let interval = Duration::from_secs(300);
assert_eq!(startup_delay(interval, None), Duration::from_secs(30));
}
#[test]
fn startup_delay_recent_run() {
let interval = Duration::from_secs(300);
let last = Utc::now() - CDuration::seconds(60);
let delay = startup_delay(interval, Some(last));
assert!(
delay >= Duration::from_secs(238) && delay <= Duration::from_secs(242),
"expected ~240 s, got {delay:?}"
);
}
#[test]
fn startup_delay_old_run() {
let interval = Duration::from_secs(300);
let last = Utc::now() - CDuration::seconds(600);
assert_eq!(startup_delay(interval, Some(last)), Duration::from_secs(30));
}
}