Skip to main content

qubit_clock/timer/
tokio_timer.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Defines a timer driven by Tokio's time driver.
9
10// qubit-style: allow coverage-cfg
11
12use crate::timer::internal::tokio_runtime_liveness::TokioRuntimeLiveness;
13use crate::timer::internal::tokio_runtime_liveness_registry::TokioRuntimeLivenessRegistry;
14use crate::timer::internal::tokio_timer_future::TokioTimerFuture;
15use crate::{
16    MonotonicClock,
17    MonotonicInstant,
18    TimeError,
19    Timer,
20    TimerFuture,
21    TimerUnavailableError,
22    TokioMonotonicClock,
23    TokioRuntimeError,
24};
25#[cfg(coverage)]
26use std::sync::atomic::{
27    AtomicBool,
28    Ordering,
29};
30use std::{
31    panic::{
32        AssertUnwindSafe,
33        catch_unwind,
34    },
35    sync::{
36        Arc,
37        OnceLock,
38    },
39    time::Duration,
40};
41use tokio::runtime::Handle;
42use tokio::time::Instant;
43
44#[cfg(coverage)]
45static PANIC_NEXT_SLEEP_POLL: AtomicBool = AtomicBool::new(false);
46
47/// Makes the next Tokio Timer sleep poll panic deterministically.
48///
49/// This coverage-only hook exercises the defensive path for an unexpected
50/// Tokio sleep panic after the shared liveness check.
51#[cfg(coverage)]
52pub fn panic_next_tokio_timer_sleep_poll() {
53    PANIC_NEXT_SLEEP_POLL.store(true, Ordering::Release);
54}
55
56/// Takes the coverage-only sleep-poll panic request.
57#[cfg(coverage)]
58pub(crate) fn take_tokio_timer_sleep_poll_panic() -> bool {
59    PANIC_NEXT_SLEEP_POLL.swap(false, Ordering::AcqRel)
60}
61
62/// An asynchronous timer backed by one Tokio runtime time driver.
63///
64/// The timer retains the source clock's exact domain, origin, and runtime
65/// capability. It enters that runtime briefly to sample time and create each
66/// Tokio sleep, so registration does not depend on the caller's ambient
67/// runtime. The returned future may be polled elsewhere, but the retained
68/// runtime must remain alive and driven until the future completes or is
69/// dropped. If it shuts down first, a pending future returns
70/// [`TimerUnavailableError::RuntimeShuttingDown`].
71///
72/// # Resolution
73///
74/// Logical deadlines preserve the full [`Duration`], but Tokio drives pending
75/// sleeps with millisecond-level scheduling granularity. This timer is not for
76/// high-resolution timing, and platform scheduling may add further delay.
77#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
78#[derive(Debug)]
79pub struct TokioTimer {
80    /// Private handle retaining the source clock domain and Tokio origin.
81    clock: TokioMonotonicClock,
82    /// Lazily resolved liveness shared by timers on the retained runtime.
83    liveness: OnceLock<Arc<TokioRuntimeLiveness>>,
84}
85
86impl TokioTimer {
87    /// Creates a timer backed by an explicit runtime handle.
88    ///
89    /// This constructor does not depend on an ambient Tokio runtime.
90    ///
91    /// # Parameters
92    ///
93    /// * `runtime` - Runtime capability providing the clock and time driver.
94    ///
95    /// # Returns
96    ///
97    /// A timer with a new clock domain backed by `runtime`.
98    ///
99    /// # Panics
100    ///
101    /// Panics if all process-wide clock-domain identifiers are exhausted.
102    #[must_use]
103    #[inline]
104    pub fn from_handle(runtime: Handle) -> Self {
105        Self {
106            clock: TokioMonotonicClock::from_handle(runtime),
107            liveness: OnceLock::new(),
108        }
109    }
110
111    /// Creates a timer by capturing the currently entered Tokio runtime.
112    ///
113    /// # Returns
114    ///
115    /// A timer with a new clock domain retaining the current runtime's handle.
116    ///
117    /// # Panics
118    ///
119    /// Panics when no Tokio runtime is entered or all process-wide clock-domain
120    /// identifiers are exhausted.
121    #[must_use]
122    #[track_caller]
123    #[inline]
124    pub fn current() -> Self {
125        Self::try_current().unwrap_or_else(|error| {
126            panic!("cannot create Tokio timer: {error}")
127        })
128    }
129
130    /// Tries to create a timer by capturing the current Tokio runtime.
131    ///
132    /// # Returns
133    ///
134    /// A timer with a new clock domain retaining the current runtime's handle.
135    ///
136    /// # Errors
137    ///
138    /// Returns [`TokioRuntimeError::NotEntered`] when no Tokio runtime is
139    /// entered.
140    ///
141    /// # Panics
142    ///
143    /// Panics if all process-wide clock-domain identifiers are exhausted.
144    #[inline]
145    pub fn try_current() -> Result<Self, TokioRuntimeError> {
146        TokioMonotonicClock::try_current().map(|clock| Self {
147            clock,
148            liveness: OnceLock::new(),
149        })
150    }
151
152    /// Creates a timer sharing the supplied Tokio clock's exact domain.
153    ///
154    /// # Parameters
155    ///
156    /// * `clock` - Tokio clock whose domain, origin, and runtime capability
157    ///   apply.
158    ///
159    /// # Returns
160    ///
161    /// A timer retaining an independent same-domain clock handle.
162    #[must_use]
163    #[inline]
164    pub fn from_clock(clock: &TokioMonotonicClock) -> Self {
165        Self {
166            clock: clock.same_domain_handle(),
167            liveness: OnceLock::new(),
168        }
169    }
170
171    /// Converts a domain-scoped deadline to its native Tokio instant.
172    ///
173    /// # Parameters
174    ///
175    /// * `deadline` - Absolute deadline in the source clock domain.
176    ///
177    /// # Returns
178    ///
179    /// The corresponding Tokio instant.
180    ///
181    /// # Errors
182    ///
183    /// Returns [`TimeError::ClockDomainMismatch`] for a foreign deadline and
184    /// [`TimeError::InstantOverflow`] when conversion overflows.
185    fn native_deadline(
186        &self,
187        deadline: MonotonicInstant,
188    ) -> Result<Instant, TimeError> {
189        deadline.validate_domain(self.clock.domain())?;
190        self.clock
191            .origin()
192            .checked_add(deadline.elapsed_since_origin())
193            .ok_or(TimeError::InstantOverflow)
194    }
195
196    /// Returns runtime liveness without a reentrant `OnceLock` initializer.
197    ///
198    /// Tokio invokes task-spawn hooks synchronously. Publishing liveness in the
199    /// registry may therefore re-enter this same timer while its first
200    /// registration is still in progress.
201    ///
202    /// # Returns
203    ///
204    /// Liveness shared by timers retaining the same Tokio runtime.
205    fn runtime_liveness(&self) -> Arc<TokioRuntimeLiveness> {
206        if let Some(liveness) = self.liveness.get() {
207            return Arc::clone(liveness);
208        }
209        let liveness = TokioRuntimeLivenessRegistry::current();
210        let _ = self.liveness.set(liveness);
211        Arc::clone(
212            self.liveness
213                .get()
214                .expect("Tokio timer liveness should be initialized"),
215        )
216    }
217
218    /// Creates the future for one native deadline while the target runtime is
219    /// entered.
220    ///
221    /// # Parameters
222    ///
223    /// * `deadline` - Fixed native Tokio deadline.
224    /// * `now` - Single current-time sample used to detect reached deadlines.
225    ///
226    /// # Returns
227    ///
228    /// An immediately ready future or a Tokio sleep paired with shared
229    /// retained-runtime liveness. Dropping the future cancels only its sleep
230    /// and releases its liveness reference; the sentinel remains active while
231    /// a timer or another pending future on that runtime retains it.
232    ///
233    /// # Errors
234    ///
235    /// Returns [`TimerUnavailableError::TimeDriverDisabled`] when a future
236    /// deadline cannot be registered because Tokio time is disabled.
237    fn schedule(
238        &self,
239        deadline: Instant,
240        now: Instant,
241    ) -> Result<TimerFuture, TimeError> {
242        if deadline <= now {
243            return Ok(Box::pin(std::future::ready(Ok(()))));
244        }
245        // Tokio 1.52 exposes no public query for whether a Handle has a time
246        // driver. Catching the constructor panic preserves a typed error in
247        // unwind builds, but the process panic hook runs first and panic=abort
248        // cannot recover. Temporarily replacing the global hook would race
249        // with application panic handling, so this library deliberately does
250        // not attempt to suppress that observable side effect.
251        let sleep = catch_unwind(AssertUnwindSafe(|| {
252            tokio::time::sleep_until(deadline)
253        }))
254        .map_err(|_| TimeError::TimerUnavailable {
255            source: TimerUnavailableError::TimeDriverDisabled,
256        })?;
257        // Criterion's `tokio_timer` benchmark showed that 10,240 legacy
258        // per-deadline sentinels retained 10,240 tasks and made registration
259        // more than 20% slower than native sleeps. One lazy sentinel per
260        // retained runtime preserves structured shutdown errors without that
261        // scaling cost.
262        let liveness = self.runtime_liveness();
263        Ok(Box::pin(TokioTimerFuture::new(sleep, liveness)))
264    }
265}
266
267impl Timer for TokioTimer {
268    /// Returns the private same-domain Tokio clock handle.
269    ///
270    /// # Returns
271    ///
272    /// The monotonic clock driving this timer.
273    #[inline(always)]
274    fn clock(&self) -> &dyn MonotonicClock {
275        &self.clock
276    }
277
278    /// Creates a Tokio sleep with a fixed absolute deadline.
279    ///
280    /// # Parameters
281    ///
282    /// * `deadline` - Deadline in this timer's clock domain.
283    ///
284    /// # Returns
285    ///
286    /// A future waiting for the fixed deadline, or an immediately ready future
287    /// for a reached deadline in the retained runtime's time domain. If the
288    /// retained runtime shuts down before a pending future completes, that
289    /// future returns [`TimerUnavailableError::RuntimeShuttingDown`].
290    ///
291    /// # Errors
292    ///
293    /// Returns a domain mismatch or instant overflow before runtime access.
294    /// Returns [`TimerUnavailableError::TimeDriverDisabled`] when a future
295    /// deadline requires a time driver that the retained runtime did not
296    /// enable. Reached deadlines do not require a time driver.
297    /// In unwind builds Tokio's panic hook may observe this disabled-driver
298    /// condition before it is converted into the structured error. In
299    /// `panic = "abort"` builds Tokio aborts before conversion is possible.
300    fn at(&self, deadline: MonotonicInstant) -> Result<TimerFuture, TimeError> {
301        let deadline = self.native_deadline(deadline)?;
302        self.clock
303            .with_runtime(|| self.schedule(deadline, Instant::now()))
304    }
305
306    /// Registers a notification after a duration in the retained Tokio
307    /// runtime.
308    ///
309    /// # Parameters
310    ///
311    /// * `duration` - Duration from the retained runtime's current instant.
312    ///
313    /// # Returns
314    ///
315    /// A future that becomes ready when the fixed deadline is reached.
316    /// If the retained runtime shuts down before that happens, the future
317    /// returns [`TimerUnavailableError::RuntimeShuttingDown`].
318    ///
319    /// # Errors
320    ///
321    /// Returns [`TimeError::InstantOverflow`] when the relative deadline cannot
322    /// be represented, or [`TimerUnavailableError::TimeDriverDisabled`] when a
323    /// nonzero future deadline requires a disabled time driver.
324    /// In unwind builds Tokio's panic hook may observe this disabled-driver
325    /// condition before it is converted into the structured error. In
326    /// `panic = "abort"` builds Tokio aborts before conversion is possible.
327    #[inline]
328    fn after(&self, duration: Duration) -> Result<TimerFuture, TimeError> {
329        self.clock.with_runtime(|| {
330            let now = Instant::now();
331            let deadline = now
332                .checked_add(duration)
333                .ok_or(TimeError::InstantOverflow)?;
334            self.schedule(deadline, now)
335        })
336    }
337}