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}