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}