Skip to main content

timeout_tracing/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    error::Error,
5    fmt::Display,
6    pin::Pin,
7    task::{Context, Poll},
8    time::Duration,
9};
10
11use pin_project_lite::pin_project;
12use tracing::{Level, span};
13
14use crate::waker::{TracingTimeoutWaker, TracingTimeoutWakerInner};
15
16pub use crate::{
17    trace::CaptureSpanAndStackTrace, trace::CaptureSpanTrace, trace::CaptureTrace,
18    trace::StackAndSpanTrace,
19};
20
21#[cfg(test)]
22mod tests;
23mod trace;
24mod waker;
25
26/// Drive the future `fut` to completion, while limiting its run time to `duration`.
27/// If `fut` fails to finish within `furation`, returns span traces for all active
28/// await points within `fut`.
29/// Use `capture` to specify which kind of trace should be captured.
30///
31/// # Examples
32/// ```rust
33/// # use std::time::Duration;
34/// # use tokio::time::sleep;
35/// # use timeout_tracing::{CaptureSpanTrace, timeout};
36/// # use tracing::instrument;
37/// # use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
38/// # tokio::runtime::Runtime::new()
39/// #     .unwrap()
40/// #     .block_on(async {
41/// tracing_subscriber::registry()
42///     .with(tracing_error::ErrorLayer::default())
43///     .init();
44///
45/// let result = timeout(Duration::from_secs(1), CaptureSpanTrace, long_computation()).await;
46/// assert_eq!(filter_span_trace(result.err().unwrap().to_string()), "timeout elapsed at:
47/// trace 0:
48///    0: example::step_2
49///              at example.rs:123
50///    1: example::long_computation
51///              at example.rs:123
52/// ");
53///
54/// #[instrument]
55/// async fn long_computation() {
56///     step_1().await;
57///     step_2().await;
58/// }
59/// #[instrument]
60/// async fn step_1() {
61///     sleep(Duration::from_millis(10)).await;
62/// }
63/// #[instrument]
64/// async fn step_2() {
65///     sleep(Duration::from_secs(2)).await;
66/// }
67/// # fn filter_span_trace(trace: String) -> String {
68/// #     use regex::Regex;
69/// #
70/// #     let re = Regex::new(r"at .*\.rs:([0-9_]+)").unwrap();
71/// #     let trace = re.replace_all(&trace, "at example.rs:123").to_string();
72/// #     let re = Regex::new(r"[a-z0-9:_]+::([a-z]+)").unwrap();
73/// #     let trace = re.replace_all(&trace, "example::$1").to_string();
74/// #     trace
75/// # }
76/// # });
77/// ```
78pub fn timeout<C, Fut>(duration: Duration, capture: C, fut: Fut) -> TimeoutFuture<C, Fut> {
79    let deadline = tokio::time::sleep(duration);
80    TimeoutFuture {
81        deadline,
82        capture: Some(capture),
83        inner: fut,
84    }
85}
86
87pin_project! {
88    pub struct TimeoutFuture<C, Fut> {
89        #[pin]
90        deadline: tokio::time::Sleep,
91        capture: Option<C>,
92        #[pin]
93        inner: Fut,
94    }
95}
96
97impl<C, Fut> Future for TimeoutFuture<C, Fut>
98where
99    C: CaptureTrace + Send + 'static,
100    Fut: Future,
101{
102    type Output = Result<Fut::Output, TimeoutElapsed<C::Trace>>;
103
104    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
105        let this = self.project();
106        // A span just so that nested timeouts had some
107        let deadline_span = span!(Level::TRACE, "deadline");
108        let guard = deadline_span.enter();
109        match this.deadline.poll(cx) {
110            Poll::Ready(()) => {
111                drop(guard);
112
113                // We hit the timeout. Do one final poll for the inner future, but collect the traces this time.
114                // TODO: we don't have to call cx.waker.clone(), but it probably does not matter much since we already hit timeout
115                let Some(capture) = this.capture.take() else {
116                    return Poll::Ready(Err(TimeoutElapsed {
117                        active_traces: Vec::new(),
118                    }));
119                };
120                let waker_inner = TracingTimeoutWakerInner::new(capture, cx.waker().clone());
121                let waker = TracingTimeoutWaker::new(waker_inner.clone()).as_std_waker();
122                let mut cx2 = Context::from_waker(&waker);
123                match this.inner.poll(&mut cx2) {
124                    Poll::Pending => {}
125                    Poll::Ready(result) => return Poll::Ready(Ok(result)),
126                }
127                let active_traces: Vec<_> = waker_inner.traces();
128                return Poll::Ready(Err(TimeoutElapsed { active_traces }));
129            }
130            Poll::Pending => {}
131        }
132        drop(guard);
133        match this.inner.poll(cx) {
134            Poll::Pending => Poll::Pending,
135            Poll::Ready(result) => Poll::Ready(Ok(result)),
136        }
137    }
138}
139
140#[derive(Debug)]
141pub struct TimeoutElapsed<Trace> {
142    pub active_traces: Vec<Trace>,
143}
144
145impl<Trace: Display> Display for TimeoutElapsed<Trace> {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        if self.active_traces.is_empty() {
148            f.write_str("timeout elapsed")?;
149        } else {
150            f.write_str("timeout elapsed at:\n")?;
151            for (idx, trace) in self.active_traces.iter().enumerate() {
152                writeln!(f, "trace {idx}:\n{trace}")?;
153            }
154        }
155        Ok(())
156    }
157}
158
159impl<Trace> Error for TimeoutElapsed<Trace> where Trace: std::fmt::Debug + std::fmt::Display {}