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}