From a56161c9e182cf92b0316a86a74df22a1c378f69 Mon Sep 17 00:00:00 2001 From: Yo Han Joo Date: Sun, 25 Jan 2026 07:42:02 +0900 Subject: [PATCH] feat: add intervals --- examples/interval.rs | 30 ++++++++++++++++++++ src/interval.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 49 +++++++++++++++++++++++++-------- 3 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 examples/interval.rs create mode 100644 src/interval.rs diff --git a/examples/interval.rs b/examples/interval.rs new file mode 100644 index 0000000..ff1b14b --- /dev/null +++ b/examples/interval.rs @@ -0,0 +1,30 @@ +use core::time::Duration; +use std::time::Instant; + +static TEST_EPOCH: std::sync::OnceLock = std::sync::OnceLock::new(); + +fn get_platform_time() -> Duration { + let epoch = TEST_EPOCH.get_or_init(Instant::now); + Instant::now().duration_since(*epoch) +} + +fn main() { + let spawner: ato::Spawner<2> = ato::Spawner::default(); + let start_time = get_platform_time(); + // Run interval task for 3 iterations (500 ms), with sleep in between, which + // should still be 1500 ms total. + ato::spawn_task!(spawner, res, { + let mut interval = + ato::interval::Interval::new(Duration::from_millis(500), get_platform_time); + for _ in 0..3 { + interval.tick().await; + // sleep for random duration less than the interval + ato::sleep(Duration::from_millis(200), get_platform_time).await; + } + }); + assert!(res.is_ok()); + assert!(spawner.run_until_all_done().is_ok()); + + let elapsed = get_platform_time() - start_time; + assert!(elapsed <= Duration::from_millis(1550)); // with some margin +} diff --git a/src/interval.rs b/src/interval.rs new file mode 100644 index 0000000..de7fc6a --- /dev/null +++ b/src/interval.rs @@ -0,0 +1,65 @@ +use core::{ + future::Future, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; + +/// A structure that ticks at regular intervals. +#[derive(Debug)] +pub struct Interval { + next_tick: Duration, + period: Duration, + time_fn: fn() -> Duration, +} + +impl Interval { + /// Creates a new `Interval` that ticks at the specified `period`. + /// + /// The first tick completes immediately (burst strategy), similar to Tokio. + pub fn new(period: Duration, time_fn: fn() -> Duration) -> Self { + let now = time_fn(); + Self { + next_tick: now, + period, + time_fn, + } + } + + /// Creates a future that completes when the next interval is reached. + /// + /// The returned future borrows the interval, allowing the `Interval` to + /// track state across multiple calls. + pub fn tick(&mut self) -> IntervalTick<'_> { + IntervalTick { interval: self } + } + + /// Resets the interval to start ticking from the current instant. + /// + /// This is useful if the system was paused or lagged significantly and + /// you want to skip the "burst" of missed ticks. + pub fn reset(&mut self) { + self.next_tick = (self.time_fn)(); + } +} + +/// A future returned by `Interval::tick`. +pub struct IntervalTick<'a> { + interval: &'a mut Interval, +} + +impl Future for IntervalTick<'_> { + type Output = (); + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let now = (this.interval.time_fn)(); + + if now >= this.interval.next_tick { + this.interval.next_tick += this.interval.period; + Poll::Ready(()) + } else { + Poll::Pending + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3cb626b..e81827b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,7 @@ #![no_std] pub mod channels; +pub mod interval; mod sleep; mod yield_now; @@ -172,11 +173,8 @@ mod tests { sleep(sleep_duration, get_current_test_time_duration).await; hello().await; }); - res.expect("Failed to spawn task"); - - if let Err(_) = spawner.run_until_all_done() { - panic!("Failed to run tasks"); - } + assert!(res.is_ok()); + assert!(spawner.run_until_all_done().is_ok()); } #[test] @@ -193,15 +191,14 @@ mod tests { } } }); - res.expect("Failed to spawn task"); + assert!(res.is_ok()); spawn_task!(spawner, res, { sleep(Duration::from_secs(1), get_current_test_time_duration).await; Q.enqueue(42).unwrap(); }); - res.expect("Failed to spawn task"); - - spawner.run_until_all_done().expect("Failed to run tasks"); + assert!(res.is_ok()); + assert!(spawner.run_until_all_done().is_ok()); } #[test] @@ -221,7 +218,7 @@ mod tests { num.push(3); } }); - res.expect("Failed to spawn task"); + assert!(res.is_ok()); let lock_clone = lock.clone(); spawn_task!(spawner, res, { @@ -230,8 +227,8 @@ mod tests { num.push(2); } }); - res.expect("Failed to spawn task"); - spawner.run_until_all_done().unwrap(); + assert!(res.is_ok()); + assert!(spawner.run_until_all_done().is_ok()); // check that the lock was accessed correctly let num = lock.lock().unwrap(); @@ -241,4 +238,32 @@ mod tests { "Lock was not accessed correctly" ); } + + #[test] + fn test_spawner_interval() { + let spawner: Spawner<8> = Spawner::default(); + + let _ = get_test_epoch(); + + let start_time = get_current_test_time_duration(); + + // Run interval task for 3 iterations (500 ms), with sleep in between, which + // should still be 1500 ms total. + spawn_task!(spawner, res, { + let mut interval = crate::interval::Interval::new( + Duration::from_millis(500), + get_current_test_time_duration, + ); + for _ in 0..3 { + interval.tick().await; + // sleep for random duration less than the interval + sleep(Duration::from_millis(200), get_current_test_time_duration).await; + } + }); + assert!(res.is_ok()); + assert!(spawner.run_until_all_done().is_ok()); + + let elapsed = get_current_test_time_duration() - start_time; + assert!(elapsed <= Duration::from_millis(1550)); // with some margin + } }