use std::num::NonZeroU64;
use std::time::Duration;
use futures::StreamExt;
use futures::stream::{self, BoxStream};
use tokio::time::{Instant, sleep_until};
use crate::cadence::next_anchor_phase_deadline;
use super::SubscriptionSource;
#[derive(Debug, Clone)]
pub enum TimerEvent {
Tick,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Timer {
interval_ms: NonZeroU64,
}
impl Timer {
#[must_use]
pub const fn new(interval_ms: NonZeroU64) -> Self {
Self { interval_ms }
}
}
impl SubscriptionSource for Timer {
type Output = TimerEvent;
type Key = NonZeroU64;
fn stream(&self) -> BoxStream<'static, TimerEvent> {
let interval = Duration::from_millis(self.interval_ms.get());
tracing::trace!(
target: "tears::subscription::time",
interval_ms = self.interval_ms.get(),
"timer stream created"
);
stream::unfold(None, move |state: Option<(Instant, Instant)>| async move {
let (anchor, deadline) = state.unwrap_or_else(|| {
let anchor = Instant::now();
(anchor, anchor + interval)
});
sleep_until(deadline).await;
let next = next_anchor_phase_deadline(anchor, interval, Instant::now());
Some((TimerEvent::Tick, Some((anchor, next))))
})
.boxed()
}
fn key(&self) -> Self::Key {
self.interval_ms
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::task::Poll;
use std::thread;
use tokio::time::{Duration, Instant, advance, timeout};
use crate::noop_waker::noop_context;
fn timer(interval_ms: u64) -> Timer {
Timer::new(NonZeroU64::new(interval_ms).expect("timer interval must be non-zero"))
}
fn poll_once(stream: &mut BoxStream<'static, TimerEvent>) -> Option<TimerEvent> {
match stream.as_mut().poll_next(&mut noop_context()) {
Poll::Ready(item) => item,
Poll::Pending => None,
}
}
#[test]
fn test_timer_new() {
let interval_ms = NonZeroU64::new(1000).expect("timer interval must be non-zero");
let timer = Timer::new(interval_ms);
assert_eq!(timer.interval_ms, interval_ms);
}
#[test]
fn test_timer_id_consistency() {
let timer1 = timer(1000);
let timer2 = timer(1000);
assert_eq!(timer1.key(), timer2.key());
}
#[test]
fn test_timer_id_different_intervals() {
let timer1 = timer(1000);
let timer2 = timer(2000);
assert_ne!(timer1.key(), timer2.key());
}
#[tokio::test(start_paused = true)]
async fn test_timer_paused_first_tick_one_interval_after_anchor() {
let mut stream = timer(2).stream();
assert!(
poll_once(&mut stream).is_none(),
"the first poll anchors and yields nothing"
);
advance(Duration::from_millis(1)).await;
assert!(
poll_once(&mut stream).is_none(),
"no tick before the first deadline"
);
advance(Duration::from_millis(1)).await;
assert!(
matches!(poll_once(&mut stream), Some(TimerEvent::Tick)),
"first tick ready one interval after the anchor"
);
}
#[tokio::test(start_paused = true)]
async fn test_timer_paused_no_catch_up_burst() {
let mut stream = timer(1).stream();
assert!(
poll_once(&mut stream).is_none(),
"the first poll anchors and yields nothing"
);
advance(Duration::from_micros(5500)).await;
assert!(
matches!(poll_once(&mut stream), Some(TimerEvent::Tick)),
"one tick ready after the multi-interval advance"
);
assert!(
poll_once(&mut stream).is_none(),
"a second tick would be a catch-up burst"
);
advance(Duration::from_micros(500)).await;
assert!(
matches!(poll_once(&mut stream), Some(TimerEvent::Tick)),
"cadence resumes at the next anchor-phase boundary"
);
}
#[tokio::test(start_paused = true)]
async fn test_timer_paused_post_miss_cadence_preserves_phase() {
let mut stream = timer(1).stream();
assert!(
poll_once(&mut stream).is_none(),
"the first poll anchors and yields nothing"
);
advance(Duration::from_millis(1)).await;
assert!(
matches!(poll_once(&mut stream), Some(TimerEvent::Tick)),
"on-time first tick"
);
advance(Duration::from_micros(2500)).await;
assert!(
matches!(poll_once(&mut stream), Some(TimerEvent::Tick)),
"late tick at 3.5 ms"
);
advance(Duration::from_micros(400)).await;
assert!(
poll_once(&mut stream).is_none(),
"no tick before the 4 ms boundary"
);
advance(Duration::from_micros(100)).await;
assert!(
matches!(poll_once(&mut stream), Some(TimerEvent::Tick)),
"tick at the preserved 4 ms anchor-phase boundary, not 4.5 ms"
);
}
#[tokio::test(start_paused = true)]
async fn test_timer_paused_anchor_is_first_poll() {
let source = timer(2);
advance(Duration::from_millis(10)).await;
let mut stream = source.stream();
advance(Duration::from_millis(10)).await;
assert!(
poll_once(&mut stream).is_none(),
"pre-first-poll time must not count against the first interval"
);
advance(Duration::from_millis(1)).await;
assert!(
poll_once(&mut stream).is_none(),
"one interval has not elapsed since the anchor"
);
advance(Duration::from_millis(1)).await;
assert!(
matches!(poll_once(&mut stream), Some(TimerEvent::Tick)),
"first deadline is first_poll_time + interval"
);
}
#[tokio::test(start_paused = true)]
async fn test_timer_stream_built_outside_the_runtime_anchors_at_first_poll() {
let mut stream = thread::spawn(|| timer(2).stream())
.join()
.expect("stream construction off-runtime should not panic");
assert!(
poll_once(&mut stream).is_none(),
"no tick at the first poll"
);
advance(Duration::from_millis(1)).await;
assert!(
poll_once(&mut stream).is_none(),
"no tick before one interval after the first poll"
);
advance(Duration::from_millis(1)).await;
assert!(
matches!(poll_once(&mut stream), Some(TimerEvent::Tick)),
"first tick one interval after the first poll, on the polling runtime's clock"
);
}
#[tokio::test]
async fn test_timer_stream_produces_ticks() {
let timer = timer(10); let mut stream = timer.stream();
let result = timeout(Duration::from_millis(100), stream.next()).await;
assert!(matches!(result, Ok(Some(TimerEvent::Tick))));
}
#[tokio::test]
async fn test_timer_stream_multiple_ticks() {
let timer = timer(10); let mut stream = timer.stream();
let mut count = 0;
for _ in 0..3 {
let result = timeout(Duration::from_millis(100), stream.next()).await;
if matches!(result, Ok(Some(TimerEvent::Tick))) {
count += 1;
}
}
assert_eq!(count, 3);
}
#[tokio::test]
async fn test_timer_interval_accuracy() {
let timer = timer(50); let mut stream = timer.stream();
let start = Instant::now();
let result = timeout(Duration::from_millis(300), stream.next()).await;
assert!(matches!(result, Ok(Some(TimerEvent::Tick))));
let first_tick = start.elapsed();
let result = timeout(Duration::from_millis(300), stream.next()).await;
assert!(matches!(result, Ok(Some(TimerEvent::Tick))));
let second_tick = start.elapsed();
assert!(
first_tick >= Duration::from_millis(30) && first_tick <= Duration::from_millis(100),
"First tick was {first_tick:?}, expected between 30-100ms",
);
assert!(
second_tick >= Duration::from_millis(80) && second_tick <= Duration::from_millis(150),
"Second tick was {second_tick:?}, expected between 80-150ms",
);
}
#[tokio::test]
async fn test_timer_no_immediate_tick() {
let timer = timer(100); let mut stream = timer.stream();
let start = Instant::now();
let result = timeout(Duration::from_millis(70), stream.next()).await;
assert!(
result.is_err(),
"Timer should not tick immediately (within 70ms)"
);
let result = timeout(Duration::from_millis(200), stream.next()).await;
assert!(
matches!(result, Ok(Some(TimerEvent::Tick))),
"Timer should tick after interval"
);
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_millis(70),
"Timer ticked too early: {elapsed:?}",
);
}
#[tokio::test]
async fn test_timer_different_intervals() {
let fast_timer = timer(20);
let slow_timer = timer(200);
let mut fast_stream = fast_timer.stream();
let mut slow_stream = slow_timer.stream();
let fast_result = timeout(Duration::from_millis(100), fast_stream.next()).await;
let slow_result = timeout(Duration::from_millis(100), slow_stream.next()).await;
assert!(
fast_result.is_ok(),
"Fast timer (20ms) should tick within 100ms"
);
assert!(
slow_result.is_err(),
"Slow timer (200ms) should not tick within 100ms"
);
}
}