From 35b6ece758e381fe03320210c16a167bcdbef3e8 Mon Sep 17 00:00:00 2001 From: saneki Date: Wed, 5 Aug 2026 23:15:55 -0400 Subject: [PATCH] Add `RealtimeDelay` for realtime clock support Very similar to `Delay` but uses a realtime clock with a provided `SystemTime` deadline and the `Abstime` flag when setting the timer. Other notes: - Declined to implement `RealtimeDelay::reset` to discourage desyncing the struct state with the potentially armed `timerfd` state. - `poll` may return an `io::Error` of kind `ErrorKind::Other` if `deadline` is before the Unix epoch. Reaching this requires the system's realtime clock to also be before the epoch for at least a short period of time so it may be impossible for this to actually occur, but I figured returning an `Err` was better than a panic. --- src/lib.rs | 2 + src/realtime_delay.rs | 123 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/realtime_delay.rs diff --git a/src/lib.rs b/src/lib.rs index 074680d..387e051 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,12 +36,14 @@ mod delay; mod delay_queue; */ mod interval; +mod realtime_delay; pub use delay::Delay; pub use interval::Interval; /* pub use delay_queue::DelayQueue; */ +pub use realtime_delay::RealtimeDelay; pub struct TimerFd(AsyncFd); diff --git a/src/realtime_delay.rs b/src/realtime_delay.rs new file mode 100644 index 0000000..5f110b4 --- /dev/null +++ b/src/realtime_delay.rs @@ -0,0 +1,123 @@ +use std::future::Future; +use std::io::{Error as IoError, ErrorKind}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::SystemTime; + +use crate::TimerFd; +use futures_core::ready; +use timerfd::{ClockId, SetTimeFlags, TimerState}; +use tokio::io::{AsyncRead, ReadBuf}; + +/// A future that completes at a specified instant in realtime. +/// Instances of [`RealtimeDelay`] perform no work and complete with () once the specified deadline has been reached. +/// [`RealtimeDelay`] is powered by `timerfd` and has a resolution of 1 nanosecond. +pub struct RealtimeDelay { + timerfd: TimerFd, + deadline: SystemTime, + cancel_on_set: bool, + initialized: bool, +} + +impl RealtimeDelay { + /// Create a new [`RealtimeDelay`] instance that elapses at `deadline`. + fn create(clock: ClockId, deadline: SystemTime, cancel_on_set: bool) -> Result { + let timerfd = TimerFd::new(clock)?; + Ok(RealtimeDelay { + timerfd, + deadline, + cancel_on_set, + initialized: false, + }) + } + + /// Create a new [`RealtimeDelay`] instance with [`ClockId::Realtime`] that elapses at `deadline`. + pub fn new(deadline: SystemTime, cancel_on_set: bool) -> Result { + Self::create(ClockId::Realtime, deadline, cancel_on_set) + } + + /// Create a new [`RealtimeDelay`] instance with [`ClockId::RealtimeAlarm`] that elapses at `deadline`. + pub fn new_alarm(deadline: SystemTime, cancel_on_set: bool) -> Result { + Self::create(ClockId::RealtimeAlarm, deadline, cancel_on_set) + } + + /// Returns the instant at which the future will complete. + pub const fn deadline(&self) -> SystemTime { + self.deadline + } + + /// Returns true if the [`RealtimeDelay`] has elapsed. + pub fn is_elapsed(&self) -> bool { + self.deadline <= SystemTime::now() + } +} + +impl Future for RealtimeDelay { + type Output = Result<(), IoError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if !self.initialized { + if self.is_elapsed() { + return Poll::Ready(Ok(())); + } + // In order for `deadline` to be before the epoch but not have elapsed, + // would require the realtime clock to have also been before the epoch + // during `is_elapsed`. + let duration = self + .deadline + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|_| IoError::from(ErrorKind::Other))?; + let flags = match self.cancel_on_set { + true => SetTimeFlags::TimerCancelOnSet, + false => SetTimeFlags::Abstime, + }; + self.timerfd.set_state(TimerState::Oneshot(duration), flags); + self.initialized = true; + } + let mut buf = [0u8; 8]; + let mut buf = ReadBuf::new(&mut buf); + ready!(Pin::new(&mut self.as_mut().timerfd).poll_read(cx, &mut buf)?); + Poll::Ready(Ok(())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, Instant}; + + #[tokio::test] + async fn realtime_delay_zero_duration() -> Result<(), std::io::Error> { + let now = Instant::now(); + let delay = RealtimeDelay::new(SystemTime::now(), false)?; + delay.await?; + let elapsed = now.elapsed(); + println!("{:?}", elapsed); + assert!(elapsed < Duration::from_millis(1)); + Ok(()) + } + + #[tokio::test] + async fn realtime_delay_works() { + let now = Instant::now(); + let deadline = SystemTime::now().checked_add(Duration::from_micros(10)).unwrap(); + let delay = RealtimeDelay::new(deadline, false).unwrap(); + delay.await.unwrap(); + let elapsed = now.elapsed(); + println!("{:?}", elapsed); + assert!(elapsed < Duration::from_millis(1)); + } + + #[tokio::test] + async fn realtime_delay_realtime_1sec_works() { + let now = Instant::now(); + let deadline = SystemTime::now().checked_add(Duration::from_secs(1)).unwrap(); + let delay = RealtimeDelay::new(deadline, false).unwrap(); + // Assume system clock is not modified in this period. + delay.await.unwrap(); + let elapsed = now.elapsed(); + println!("{:?}", elapsed); + let expected = Duration::from_secs(1)..Duration::from_secs(2); + assert!(expected.contains(&elapsed)); + } +}