Skip to main content

sui_futures/
timeout_trace.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Wraps a future with a timeout that also captures a [`SpanTrace`] of every await point that
5//! was still pending when the deadline fired, by piggy-backing on the [`Waker`] contract.
6//!
7//! ## The technique
8//!
9//! Any `Future::poll` that returns [`Poll::Pending`] is required, by the `Waker` contract, to
10//! arrange for its task to be polled again later -- almost always by cloning the `Waker` it was
11//! given and stashing the clone somewhere (a timer wheel, an I/O reactor's readiness slot, a
12//! channel's waiter list, ...) so it can be woken once whatever it's waiting on is ready.
13//!
14//! [`timeout`] exploits this. Once its deadline elapses, it polls the wrapped future exactly one
15//! more time using a custom [`Waker`] (backed by a hand-rolled [`RawWakerVTable`], see
16//! `TracingWaker`) that captures a [`SpanTrace`] every time it's cloned. Each clone marks one
17//! distinct still-pending await point -- there can be more than one, e.g. when the future
18//! resolves several sibling branches concurrently. If that final poll is still `Pending`, every
19//! trace captured during it (for clones that are still alive by the time we look) is returned
20//! via [`TimeoutElapsed::active_traces`].
21//!
22//! Because this technique is only ever used for a single, final poll -- whatever it returns,
23//! [`timeout`] is done, either way (see [`TracedTimeout::poll`]) -- `TracingWaker` never
24//! forwards real wake-ups anywhere. There's no task left that a later wake could usefully
25//! re-poll: by the time any retained clone is woken, `timeout`'s own future has either already
26//! resolved and been dropped, or is about to resolve on its own regardless.
27
28use std::error::Error;
29use std::fmt::Display;
30use std::future::Future;
31use std::pin::Pin;
32use std::sync::Arc;
33use std::sync::Weak;
34use std::task::Context;
35use std::task::Poll;
36use std::task::RawWaker;
37use std::task::RawWakerVTable;
38use std::task::Waker;
39use std::time::Duration;
40
41use pin_project::pin_project;
42use tokio::sync::mpsc;
43use tokio::time::Sleep;
44use tracing_error::SpanTrace;
45
46/// Wraps a future with a timeout that also captures [`SpanTrace`]s of whatever was still pending
47/// when it fired -- see [`timeout`].
48#[pin_project]
49pub struct TracedTimeout<Fut> {
50    #[pin]
51    deadline: Sleep,
52    #[pin]
53    inner: Fut,
54}
55
56/// Returned when `inner` hasn't finished within `duration`, carrying a [`SpanTrace`] for every
57/// await point of `inner` that was still pending at that moment. There can be more than one --
58/// e.g. when a caller resolves several sibling branches concurrently.
59#[derive(Debug)]
60pub struct TimeoutElapsed {
61    pub active_traces: Vec<SpanTrace>,
62}
63
64/// One trace captured by a [`TracingWaker`] clone, in transit through the channel described
65/// there.
66struct TracedAwaitPoint {
67    trace: SpanTrace,
68    /// Upgrades for as long as the [`TracingWaker`] clone that produced this entry is still
69    /// alive; see that type's docs for why that matters.
70    alive: Weak<()>,
71}
72
73/// A `Waker` that captures a [`SpanTrace`] every time it's cloned -- per the module docs, each
74/// clone marks a distinct still-pending await point -- and does nothing else: `wake`,
75/// `wake_by_ref`, and `drop` only free the clone's own allocation, forwarding no wake-up to
76/// anything. That's sound here specifically because a `TracingWaker` is only ever handed to a
77/// single, final poll after `timeout`'s deadline has already elapsed (see
78/// [`TracedTimeout::poll`]): that poll's result is final regardless of what it is, so there's
79/// never a task left for a later wake-up to usefully reach.
80struct TracingWaker {
81    /// Traces are sent, one per clone, down this channel; `TracedTimeout::poll` drains it after
82    /// the final poll returns. Unbounded: sends only ever happen synchronously during the one
83    /// poll call a `TracingWaker` is used for, and are always fully drained immediately
84    /// afterward, so there's no producer that could outpace a slow consumer -- the usual reason
85    /// the repo disallows `mpsc::unbounded_channel` (see the `#[allow]` where this is
86    /// constructed).
87    sender: mpsc::UnboundedSender<TracedAwaitPoint>,
88    /// Kept alive for as long as this clone is -- e.g. by whatever external structure (a timer,
89    /// a connection pool's waiter list, ...) retained the `Waker` clone wrapping it (never read
90    /// directly -- `TracedAwaitPoint::alive` observes it through the paired `Weak` handle).
91    /// Paired with a `Weak` handle sent alongside this clone's trace, so a trace only "counts" as
92    /// an active await point if the clone that produced it is still alive by drain time: a clone
93    /// that's created and then immediately dropped without being retained anywhere represents a
94    /// future that merely touched its waker without truly registering to be woken by it, and
95    /// shouldn't be reported as a pending await point. `None` for the original (root) waker,
96    /// which isn't itself an await point and never produces a trace.
97    _alive: Option<Arc<()>>,
98}
99
100impl TracingWaker {
101    const VTABLE: RawWakerVTable = RawWakerVTable::new(
102        Self::raw_clone,
103        Self::raw_wake,
104        Self::raw_wake_by_ref,
105        Self::raw_drop,
106    );
107
108    fn new_std_waker(sender: mpsc::UnboundedSender<TracedAwaitPoint>) -> Waker {
109        let data = Box::into_raw(Box::new(Self {
110            sender,
111            _alive: None,
112        }));
113        // SAFETY: `data` was just obtained from `Box::into_raw`, so it's a unique, valid pointer
114        // to a `Box<Self>` -- the precondition every vtable function below relies on.
115        unsafe { Waker::new(data as *const (), &Self::VTABLE) }
116    }
117
118    fn clone_capturing_trace(&self) -> Box<Self> {
119        let alive = Arc::new(());
120        // Ignore errors: the only way `send` fails here is a closed receiver, meaning
121        // `TracedTimeout::poll` has already drained and returned -- there's nowhere useful to put
122        // this trace. Dropping it here is never unsafe, only lossy: the timeout still resolves
123        // and its response still goes out fine either way, just possibly missing this one entry
124        // from the trace list.
125        let _ = self.sender.send(TracedAwaitPoint {
126            trace: SpanTrace::capture(),
127            alive: Arc::downgrade(&alive),
128        });
129        Box::new(Self {
130            sender: self.sender.clone(),
131            _alive: Some(alive),
132        })
133    }
134
135    unsafe fn raw_clone(data: *const ()) -> RawWaker {
136        // SAFETY: see `new_std_waker`.
137        let this = unsafe { &*(data as *const Self) };
138        let cloned = this.clone_capturing_trace();
139        RawWaker::new(Box::into_raw(cloned) as *const (), &Self::VTABLE)
140    }
141
142    unsafe fn raw_wake(data: *const ()) {
143        // No wake-up to forward -- see the type docs -- so `wake` reduces to freeing the box,
144        // exactly like `drop` does.
145        unsafe { Self::raw_drop(data) };
146    }
147
148    unsafe fn raw_wake_by_ref(_data: *const ()) {
149        // Nothing to notify; see the type docs.
150    }
151
152    unsafe fn raw_drop(data: *const ()) {
153        // SAFETY: see `new_std_waker`.
154        drop(unsafe { Box::<Self>::from_raw(data as *mut Self) });
155    }
156}
157
158impl<Fut: Future> Future for TracedTimeout<Fut> {
159    type Output = Result<Fut::Output, TimeoutElapsed>;
160
161    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
162        let this = self.project();
163
164        if this.deadline.poll(cx).is_pending() {
165            return this.inner.poll(cx).map(Ok);
166        }
167
168        // We hit the timeout. Do one final poll of `inner`, capturing a trace every time it
169        // clones its waker. This call resolves this whole `poll` one way or another -- see
170        // `TracingWaker`'s docs for why that means it never needs to forward wake-ups.
171        // Unbounded: see `TracingWaker::sender`'s docs for why that's safe here despite the
172        // repo's default of disallowing `mpsc::unbounded_channel`.
173        #[allow(clippy::disallowed_methods)]
174        let (sender, mut receiver) = mpsc::unbounded_channel();
175        let waker = TracingWaker::new_std_waker(sender);
176        let mut traced_cx = Context::from_waker(&waker);
177        match this.inner.poll(&mut traced_cx) {
178            Poll::Ready(result) => Poll::Ready(Ok(result)),
179            Poll::Pending => {
180                let mut active_traces = Vec::new();
181                while let Ok(TracedAwaitPoint { trace, alive }) = receiver.try_recv() {
182                    if alive.upgrade().is_some() {
183                        active_traces.push(trace);
184                    }
185                }
186                Poll::Ready(Err(TimeoutElapsed { active_traces }))
187            }
188        }
189    }
190}
191
192impl Display for TimeoutElapsed {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        if self.active_traces.is_empty() {
195            f.write_str("timeout elapsed")?;
196        } else {
197            f.write_str("timeout elapsed at:\n")?;
198            for (idx, trace) in self.active_traces.iter().enumerate() {
199                writeln!(f, "trace {idx}:\n{trace}")?;
200            }
201        }
202        Ok(())
203    }
204}
205
206impl Error for TimeoutElapsed {}
207
208/// Drive `fut` to completion, limiting its run time to `duration`. If `fut` doesn't finish in
209/// time, returns [`TimeoutElapsed`] with a trace of every await point still pending at that
210/// moment.
211pub fn timeout<Fut>(duration: Duration, fut: Fut) -> TracedTimeout<Fut> {
212    TracedTimeout {
213        deadline: tokio::time::sleep(duration),
214        inner: fut,
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use std::future::Future;
221    use std::sync::Arc;
222    use std::sync::Mutex;
223    use std::task::Poll;
224    use std::task::Waker;
225    use std::time::Duration;
226
227    use tokio::sync::mpsc;
228    use tracing::instrument;
229    use tracing_error::ErrorLayer;
230    use tracing_subscriber::layer::SubscriberExt;
231
232    use super::TracingWaker;
233    use super::timeout;
234
235    /// Runs `fut` with a `tracing_error::ErrorLayer` installed, so [`SpanTrace::capture`] (used
236    /// internally by [`timeout`]) picks up the `#[instrument]`ed spans below. Relies on
237    /// `#[tokio::test]`'s default current-thread runtime so the thread-local default subscriber
238    /// stays in effect across every poll of `fut`.
239    async fn with_error_layer<Fut: Future>(fut: Fut) -> Fut::Output {
240        let subscriber = tracing_subscriber::registry().with(ErrorLayer::default());
241        let _guard = tracing::subscriber::set_default(subscriber);
242        fut.await
243    }
244
245    /// Never completes and never touches its waker -- an await point with nothing registered to
246    /// wake it.
247    #[instrument]
248    async fn pending_forever() {
249        std::future::pending::<()>().await
250    }
251
252    /// Never completes; clones its waker into `slot` on every poll and keeps the clone alive, the
253    /// way a future genuinely waiting on some external event would.
254    #[instrument(skip(slot))]
255    async fn awaits_retained_waker(slot: Arc<Mutex<Option<Waker>>>) {
256        std::future::poll_fn(move |cx| {
257            *slot.lock().unwrap() = Some(cx.waker().clone());
258            Poll::<()>::Pending
259        })
260        .await
261    }
262
263    #[instrument(skip(slot))]
264    async fn branch_a(slot: Arc<Mutex<Option<Waker>>>) {
265        awaits_retained_waker(slot).await
266    }
267
268    #[instrument(skip(slot))]
269    async fn branch_b(slot: Arc<Mutex<Option<Waker>>>) {
270        awaits_retained_waker(slot).await
271    }
272
273    /// Never completes; clones its waker on every poll but immediately drops the clone, the way a
274    /// future that merely inspects (rather than retains) its waker would.
275    #[instrument]
276    async fn drops_waker_immediately() {
277        std::future::poll_fn(|cx| {
278            let _ = cx.waker().clone();
279            Poll::<()>::Pending
280        })
281        .await
282    }
283
284    /// Never completes; on every poll, clones its waker once and immediately drops that clone
285    /// (touched but not retained), then clones it again and genuinely retains the second clone --
286    /// covering a future whose single poll call touches the waker more than once, only one of
287    /// which is a real await-point registration.
288    #[instrument(skip(slot))]
289    async fn drops_then_retains_waker(slot: Arc<Mutex<Option<Waker>>>) {
290        std::future::poll_fn(move |cx| {
291            let _ = cx.waker().clone();
292            *slot.lock().unwrap() = Some(cx.waker().clone());
293            Poll::<()>::Pending
294        })
295        .await
296    }
297
298    #[tokio::test]
299    async fn completes_before_deadline() {
300        let result = timeout(Duration::from_secs(10), async { 42 }).await;
301        assert_eq!(result.unwrap(), 42);
302    }
303
304    #[tokio::test]
305    async fn elapses_with_no_pending_trace() {
306        let err = with_error_layer(timeout(Duration::from_millis(10), pending_forever()))
307            .await
308            .unwrap_err();
309
310        assert!(err.active_traces.is_empty());
311        assert_eq!(err.to_string(), "timeout elapsed");
312    }
313
314    #[tokio::test]
315    async fn elapses_with_one_pending_trace() {
316        let slot = Arc::new(Mutex::new(None));
317        let err = with_error_layer(timeout(
318            Duration::from_millis(10),
319            awaits_retained_waker(slot),
320        ))
321        .await
322        .unwrap_err();
323
324        assert_eq!(err.active_traces.len(), 1);
325        assert!(err.to_string().contains("awaits_retained_waker"));
326    }
327
328    #[tokio::test]
329    async fn elapses_with_multiple_pending_traces() {
330        let err = with_error_layer(timeout(
331            Duration::from_millis(10),
332            futures::future::join(
333                branch_a(Arc::new(Mutex::new(None))),
334                branch_b(Arc::new(Mutex::new(None))),
335            ),
336        ))
337        .await
338        .unwrap_err();
339
340        assert_eq!(err.active_traces.len(), 2);
341        let rendered = err.to_string();
342        assert!(rendered.contains("branch_a"));
343        assert!(rendered.contains("branch_b"));
344    }
345
346    #[tokio::test]
347    async fn dropped_waker_leaves_no_trace() {
348        let err = with_error_layer(timeout(
349            Duration::from_millis(10),
350            drops_waker_immediately(),
351        ))
352        .await
353        .unwrap_err();
354
355        assert!(
356            err.active_traces.is_empty(),
357            "a waker clone dropped before the timeout's final poll returns shouldn't leave a \
358             trace behind"
359        );
360    }
361
362    #[tokio::test]
363    async fn only_retained_clone_leaves_a_trace() {
364        let slot = Arc::new(Mutex::new(None));
365        let err = with_error_layer(timeout(
366            Duration::from_millis(10),
367            drops_then_retains_waker(slot),
368        ))
369        .await
370        .unwrap_err();
371
372        assert_eq!(
373            err.active_traces.len(),
374            1,
375            "the touched-but-dropped clone shouldn't leave a trace; only the retained one should"
376        );
377    }
378
379    /// Regression test: `wake()` must drop its boxed waker just like `wake_by_ref()` + `drop()`
380    /// or `drop()` alone do, per the `RawWakerVTable` contract. Before the fix, `raw_wake` only
381    /// borrowed the box, leaking it forever.
382    #[test]
383    fn wake_drops_the_waker_like_drop_does() {
384        #[allow(clippy::disallowed_methods)]
385        let (sender, receiver) = mpsc::unbounded_channel();
386        let waker = TracingWaker::new_std_waker(sender);
387        assert_eq!(receiver.sender_strong_count(), 1, "waker's own box");
388
389        // Simulate a future registering for a wakeup elsewhere (e.g. a DB driver's completion
390        // slot) by cloning the waker.
391        let cloned = waker.clone();
392        assert_eq!(receiver.sender_strong_count(), 2);
393
394        // ...and later consuming it via `wake()`, as one-shot completion notifications do.
395        cloned.wake();
396        assert_eq!(
397            receiver.sender_strong_count(),
398            1,
399            "wake() must free its box, not just notify"
400        );
401
402        drop(waker);
403        assert_eq!(receiver.sender_strong_count(), 0);
404    }
405
406    /// `wake_by_ref` must not consume or free the box (unlike `wake()` above); a later explicit
407    /// `drop` still must.
408    #[test]
409    fn wake_by_ref_then_drop_frees_the_box() {
410        #[allow(clippy::disallowed_methods)]
411        let (sender, receiver) = mpsc::unbounded_channel();
412        let waker = TracingWaker::new_std_waker(sender);
413        let cloned = waker.clone();
414        assert_eq!(receiver.sender_strong_count(), 2);
415
416        cloned.wake_by_ref();
417        assert_eq!(
418            receiver.sender_strong_count(),
419            2,
420            "wake_by_ref must not free the box"
421        );
422
423        drop(cloned);
424        assert_eq!(receiver.sender_strong_count(), 1);
425        drop(waker);
426        assert_eq!(receiver.sender_strong_count(), 0);
427    }
428
429    /// Regression test: cloning the waker after its receiver has already been dropped (e.g.
430    /// because `TracedTimeout::poll` already drained and returned) must not panic -- the send is
431    /// best-effort.
432    #[test]
433    fn clone_after_receiver_dropped_does_not_panic() {
434        #[allow(clippy::disallowed_methods)]
435        let (sender, receiver) = mpsc::unbounded_channel();
436        drop(receiver);
437
438        let waker = TracingWaker::new_std_waker(sender);
439        let cloned = waker.clone();
440        drop(cloned);
441        drop(waker);
442    }
443}