use rand::Rng;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct LoopScheduler {
interval: Duration,
min_delay: Duration,
max_delay: Duration,
}
impl LoopScheduler {
pub fn new(interval: Duration, min_delay: Duration, max_delay: Duration) -> Self {
let (actual_min, actual_max) = if min_delay <= max_delay {
(min_delay, max_delay)
} else {
tracing::warn!(
min_ms = min_delay.as_millis() as u64,
max_ms = max_delay.as_millis() as u64,
"min_delay > max_delay, swapping values"
);
(max_delay, min_delay)
};
Self {
interval,
min_delay: actual_min,
max_delay: actual_max,
}
}
pub fn next_delay(&self) -> Duration {
let jitter = if self.min_delay == self.max_delay {
self.min_delay
} else {
let min_ms = self.min_delay.as_millis() as u64;
let max_ms = self.max_delay.as_millis() as u64;
Duration::from_millis(rand::rng().random_range(min_ms..=max_ms))
};
self.interval + jitter
}
pub async fn tick(&self) {
let delay = self.next_delay();
tracing::debug!(
delay_ms = delay.as_millis() as u64,
interval_ms = self.interval.as_millis() as u64,
"Scheduler tick sleeping"
);
tokio::time::sleep(delay).await;
}
pub fn interval(&self) -> Duration {
self.interval
}
pub fn jitter_range(&self) -> (Duration, Duration) {
(self.min_delay, self.max_delay)
}
}
pub fn scheduler_from_config(
interval_seconds: u64,
min_delay_seconds: u64,
max_delay_seconds: u64,
) -> LoopScheduler {
LoopScheduler::new(
Duration::from_secs(interval_seconds),
Duration::from_secs(min_delay_seconds),
Duration::from_secs(max_delay_seconds),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn next_delay_within_bounds() {
let scheduler = LoopScheduler::new(
Duration::from_secs(10),
Duration::from_secs(1),
Duration::from_secs(5),
);
for _ in 0..100 {
let delay = scheduler.next_delay();
assert!(delay >= Duration::from_secs(11)); assert!(delay <= Duration::from_secs(15)); }
}
#[test]
fn next_delay_fixed_jitter() {
let scheduler = LoopScheduler::new(
Duration::from_secs(5),
Duration::from_secs(2),
Duration::from_secs(2),
);
for _ in 0..10 {
assert_eq!(scheduler.next_delay(), Duration::from_secs(7));
}
}
#[test]
fn next_delay_zero_jitter() {
let scheduler = LoopScheduler::new(Duration::from_secs(5), Duration::ZERO, Duration::ZERO);
assert_eq!(scheduler.next_delay(), Duration::from_secs(5));
}
#[test]
fn constructor_swaps_inverted_min_max() {
let scheduler = LoopScheduler::new(
Duration::from_secs(10),
Duration::from_secs(5), Duration::from_secs(1),
);
let (min, max) = scheduler.jitter_range();
assert_eq!(min, Duration::from_secs(1));
assert_eq!(max, Duration::from_secs(5));
}
#[test]
fn interval_accessor() {
let scheduler = LoopScheduler::new(Duration::from_secs(42), Duration::ZERO, Duration::ZERO);
assert_eq!(scheduler.interval(), Duration::from_secs(42));
}
#[test]
fn scheduler_from_config_creates_correctly() {
let scheduler = scheduler_from_config(300, 30, 120);
assert_eq!(scheduler.interval(), Duration::from_secs(300));
let (min, max) = scheduler.jitter_range();
assert_eq!(min, Duration::from_secs(30));
assert_eq!(max, Duration::from_secs(120));
}
#[tokio::test]
async fn tick_completes() {
let scheduler =
LoopScheduler::new(Duration::from_millis(10), Duration::ZERO, Duration::ZERO);
let start = tokio::time::Instant::now();
scheduler.tick().await;
let elapsed = start.elapsed();
assert!(elapsed >= Duration::from_millis(10));
}
#[test]
fn next_delay_with_large_jitter() {
let scheduler = LoopScheduler::new(
Duration::from_secs(60),
Duration::from_secs(30),
Duration::from_secs(120),
);
for _ in 0..50 {
let delay = scheduler.next_delay();
assert!(delay >= Duration::from_secs(90)); assert!(delay <= Duration::from_secs(180)); }
}
#[test]
fn scheduler_clone() {
let scheduler = LoopScheduler::new(
Duration::from_secs(10),
Duration::from_secs(1),
Duration::from_secs(5),
);
let cloned = scheduler.clone();
assert_eq!(cloned.interval(), scheduler.interval());
assert_eq!(cloned.jitter_range(), scheduler.jitter_range());
}
#[test]
fn scheduler_debug() {
let scheduler = LoopScheduler::new(Duration::from_secs(1), Duration::ZERO, Duration::ZERO);
let debug = format!("{:?}", scheduler);
assert!(debug.contains("LoopScheduler"));
}
#[test]
fn scheduler_from_config_zero_interval() {
let scheduler = scheduler_from_config(0, 0, 0);
assert_eq!(scheduler.interval(), Duration::ZERO);
assert_eq!(scheduler.next_delay(), Duration::ZERO);
}
#[test]
fn scheduler_from_config_swapped_delays() {
let scheduler = scheduler_from_config(10, 60, 30);
let (min, max) = scheduler.jitter_range();
assert_eq!(min, Duration::from_secs(30));
assert_eq!(max, Duration::from_secs(60));
}
#[test]
fn scheduler_equal_min_max_deterministic() {
let scheduler = LoopScheduler::new(
Duration::from_secs(100),
Duration::from_secs(50),
Duration::from_secs(50),
);
for _ in 0..10 {
assert_eq!(scheduler.next_delay(), Duration::from_secs(150));
}
}
#[tokio::test]
async fn tick_with_jitter() {
let scheduler = LoopScheduler::new(
Duration::from_millis(5),
Duration::from_millis(1),
Duration::from_millis(5),
);
let start = tokio::time::Instant::now();
scheduler.tick().await;
let elapsed = start.elapsed();
assert!(elapsed >= Duration::from_millis(6)); }
}