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