Skip to main content

turbo_tasks/
event.rs

1use std::{
2    fmt::{Debug, Display, Formatter},
3    future::Future,
4    mem::replace,
5    pin::Pin,
6};
7#[cfg(feature = "hanging_detection")]
8use std::{
9    sync::Arc,
10    task::{Poll, ready},
11    time::Duration,
12};
13
14#[cfg(feature = "hanging_detection")]
15use tokio::time::{Timeout, timeout};
16
17/// Blocks the current thread until `listener` is notified.
18///
19/// `event-listener` gates its own blocking `wait()` behind `not(target_family = "wasm")`, which
20/// excludes every wasm target even though `wasm32-wasip1-threads` has real threads that can park.
21/// Its native implementation is just "register a waker, then park in a loop", so on wasm we drive
22/// the listener's `Future` to completion with a parking executor, which is the same mechanism.
23fn block_on_listener(listener: event_listener::EventListener) {
24    #[cfg(not(target_family = "wasm"))]
25    {
26        use event_listener::Listener as _;
27
28        listener.wait();
29    }
30    #[cfg(target_family = "wasm")]
31    futures::executor::block_on(listener);
32}
33
34pub trait EventDescriptor {
35    #[cfg(feature = "hanging_detection")]
36    fn get_description(self) -> Arc<dyn Fn() -> String + Sync + Send>;
37}
38
39impl<T, InnerFn> EventDescriptor for T
40where
41    T: FnOnce() -> InnerFn,
42    InnerFn: Fn() -> String + Sync + Send + 'static,
43{
44    #[cfg(feature = "hanging_detection")]
45    fn get_description(self) -> Arc<dyn Fn() -> String + Sync + Send> {
46        Arc::new((self)())
47    }
48}
49
50#[derive(Clone)]
51pub struct EventDescription {
52    #[cfg(feature = "hanging_detection")]
53    description: Arc<dyn Fn() -> String + Sync + Send>,
54}
55
56impl EventDescription {
57    #[inline(always)]
58    pub fn new<InnerFn>(#[allow(unused_variables)] description: impl FnOnce() -> InnerFn) -> Self
59    where
60        InnerFn: Fn() -> String + Sync + Send + 'static,
61    {
62        Self {
63            #[cfg(feature = "hanging_detection")]
64            description: Arc::new((description)()),
65        }
66    }
67}
68
69impl Display for EventDescription {
70    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
71        #[cfg(not(feature = "hanging_detection"))]
72        return write!(f, "");
73
74        #[cfg(feature = "hanging_detection")]
75        return write!(f, "{}", (self.description)());
76    }
77}
78
79impl EventDescriptor for EventDescription {
80    #[cfg(feature = "hanging_detection")]
81    fn get_description(self) -> Arc<dyn Fn() -> String + Sync + Send> {
82        self.description
83    }
84}
85
86pub struct Event {
87    #[cfg(feature = "hanging_detection")]
88    description: Arc<dyn Fn() -> String + Sync + Send>,
89    event: event_listener::Event,
90}
91
92impl Event {
93    /// See [`event_listener::Event::new`]. May attach a description that may optionally be read
94    /// later.
95    ///
96    /// This confusingly takes a closure ([`FnOnce`]) that returns a nested closure ([`Fn`]).
97    ///
98    /// When `hanging_detection` is disabled, `description` is never called.
99    ///
100    /// When `hanging_detection` is enabled, the outer closure is called immediately. The outer
101    /// closure can have an ephemeral lifetime. The inner closure must be `'static`, but is called
102    /// only when the `description` is actually read.
103    ///
104    /// The outer closure allows avoiding extra lookups (e.g. task type info) that may be needed to
105    /// capture information needed for constructing (moving into) the inner closure.
106    #[inline(always)]
107    pub fn new(#[allow(unused_variables)] description: impl EventDescriptor) -> Self {
108        #[cfg(not(feature = "hanging_detection"))]
109        return Self {
110            event: event_listener::Event::new(),
111        };
112        #[cfg(feature = "hanging_detection")]
113        return Self {
114            description: description.get_description(),
115            event: event_listener::Event::new(),
116        };
117    }
118
119    /// See [`event_listener::Event::listen`].
120    pub fn listen(&self) -> EventListener {
121        #[cfg(not(feature = "hanging_detection"))]
122        return EventListener {
123            listener: self.event.listen(),
124        };
125        #[cfg(feature = "hanging_detection")]
126        return EventListener {
127            description: self.description.clone(),
128            note: Arc::new(String::new),
129            future: Some(Box::pin(timeout(
130                Duration::from_secs(30),
131                self.event.listen(),
132            ))),
133            duration: Duration::from_secs(30),
134        };
135    }
136
137    /// See [`event_listener::Event::listen`]. May attach a note that may optionally be read later.
138    ///
139    /// This confusingly takes a closure ([`FnOnce`]) that returns a nested closure ([`Fn`]).
140    ///
141    /// When `hanging_detection` is disabled, `note` is never called.
142    ///
143    /// When `hanging_detection` is enabled, the outer closure is called immediately. The outer
144    /// closure can have an ephemeral lifetime. The inner closer must be `'static`, but is called
145    /// only when the `note` is actually read.
146    ///
147    /// The outer closure allow avoiding extra lookups (e.g. task type info) that may be needed to
148    /// capture information needed for constructing (moving into) the inner closure.
149    pub fn listen_with_note(&self, _note: impl EventDescriptor) -> EventListener {
150        #[cfg(not(feature = "hanging_detection"))]
151        return EventListener {
152            listener: self.event.listen(),
153        };
154        #[cfg(feature = "hanging_detection")]
155        return EventListener {
156            description: self.description.clone(),
157            note: _note.get_description(),
158            future: Some(Box::pin(timeout(
159                Duration::from_secs(30),
160                self.event.listen(),
161            ))),
162            duration: Duration::from_secs(30),
163        };
164    }
165
166    /// pulls out the event listener, leaving a new, empty event in its place.
167    pub fn take(&mut self) -> Event {
168        #[cfg(not(feature = "hanging_detection"))]
169        return Self {
170            event: replace(&mut self.event, event_listener::Event::new()),
171        };
172        #[cfg(feature = "hanging_detection")]
173        return Self {
174            description: self.description.clone(),
175            event: replace(&mut self.event, event_listener::Event::new()),
176        };
177    }
178}
179
180impl Event {
181    /// see [`event_listener::Event::notify`]
182    pub fn notify(&self, n: usize) {
183        self.event.notify(n);
184    }
185}
186
187impl Debug for Event {
188    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
189        let mut t = f.debug_tuple("Event");
190        #[cfg(feature = "hanging_detection")]
191        t.field(&(self.description)());
192        t.finish()
193    }
194}
195
196#[cfg(not(feature = "hanging_detection"))]
197pub struct EventListener {
198    listener: event_listener::EventListener,
199}
200
201#[cfg(not(feature = "hanging_detection"))]
202impl Debug for EventListener {
203    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
204        f.debug_tuple("EventListener").finish()
205    }
206}
207
208#[cfg(not(feature = "hanging_detection"))]
209impl Future for EventListener {
210    type Output = ();
211
212    fn poll(
213        self: Pin<&mut Self>,
214        cx: &mut std::task::Context<'_>,
215    ) -> std::task::Poll<Self::Output> {
216        let listener = unsafe { self.map_unchecked_mut(|s| &mut s.listener) };
217        listener.poll(cx)
218    }
219}
220
221#[cfg(not(feature = "hanging_detection"))]
222impl EventListener {
223    /// Blocks the current thread until the event is notified.
224    ///
225    /// This is the synchronous equivalent of `.await`-ing the `EventListener`.
226    /// Only valid in synchronous contexts (e.g. backend operations).
227    pub fn wait(self) {
228        block_on_listener(self.listener);
229    }
230}
231
232#[cfg(feature = "hanging_detection")]
233pub struct EventListener {
234    description: Arc<dyn Fn() -> String + Sync + Send>,
235    note: Arc<dyn Fn() -> String + Sync + Send>,
236    // Timeout need to stay pinned while polling and also while it's dropped.
237    // So it's important to put it into a pinned Box to be able to take it out of the Option.
238    future: Option<std::pin::Pin<Box<Timeout<event_listener::EventListener>>>>,
239    duration: Duration,
240}
241
242#[cfg(feature = "hanging_detection")]
243impl Debug for EventListener {
244    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
245        let mut t = f.debug_tuple("EventListener");
246        t.field(&(self.description)());
247        let note = (self.note)();
248        if !note.is_empty() {
249            t.field(&note);
250        }
251        t.finish()
252    }
253}
254
255#[cfg(feature = "hanging_detection")]
256impl Future for EventListener {
257    type Output = ();
258
259    fn poll(
260        mut self: Pin<&mut Self>,
261        cx: &mut std::task::Context<'_>,
262    ) -> std::task::Poll<Self::Output> {
263        while let Some(future) = self.future.as_mut() {
264            match ready!(future.as_mut().poll(cx)) {
265                Ok(_) => {
266                    self.future = None;
267                    return Poll::Ready(());
268                }
269                Err(_) => {
270                    let note = (self.note)();
271                    let description = (self.description)();
272                    if note.is_empty() {
273                        eprintln!(
274                            "EventListener({}) is potentially hanging, waiting for {}s",
275                            description,
276                            self.duration.as_secs(),
277                        );
278                    } else {
279                        eprintln!(
280                            "EventListener({}) is potentially hanging, waiting for {}s from {}",
281                            description,
282                            self.duration.as_secs(),
283                            note
284                        );
285                    }
286                    self.duration *= 2;
287                    // SAFETY: Taking from Option is safe because the value is inside of a pinned
288                    // Box. Pinning must continue until dropped.
289                    let future = self.future.take().unwrap();
290                    self.future = Some(Box::pin(timeout(
291                        self.duration,
292                        // SAFETY: We can move the inner future since it's an EventListener and
293                        // that is Unpin.
294                        unsafe { std::pin::Pin::into_inner_unchecked(future) }.into_inner(),
295                    )));
296                }
297            }
298        }
299        // EventListener was awaited again after completion
300        Poll::Ready(())
301    }
302}
303
304#[cfg(feature = "hanging_detection")]
305impl EventListener {
306    /// Blocks the current thread until the event is notified.
307    ///
308    /// Note: In `hanging_detection` builds, timeout warnings are not emitted
309    /// for sync waits (only for async `.await` usage).
310    pub fn wait(mut self) {
311        if let Some(future) = self.future.take() {
312            // SAFETY: EventListener is Unpin, so it's safe to move out of the Pin.
313            block_on_listener(unsafe { std::pin::Pin::into_inner_unchecked(future) }.into_inner());
314        }
315    }
316}
317
318#[cfg(all(test, not(feature = "hanging_detection")))]
319mod tests {
320    use std::{
321        hint::black_box,
322        sync::{
323            Arc,
324            atomic::{AtomicBool, Ordering},
325        },
326        time::Instant,
327    };
328
329    use tokio::time::{Duration, timeout};
330
331    use super::*;
332
333    // The closures used for descriptions/notes should be eliminated. This may only happen at higher
334    // optimization levels (that would be okay), but in practice it seems to work even for
335    // opt-level=0.
336    #[tokio::test]
337    async fn ensure_dead_code_elimination() {
338        fn dead_fn() {
339            // This code triggers a build error when it's not removed.
340            unsafe {
341                unsafe extern "C" {
342                    fn trigger_link_error() -> !;
343                }
344                trigger_link_error();
345            }
346        }
347
348        let event = black_box(Event::new(|| {
349            dead_fn();
350            || {
351                dead_fn();
352                String::new()
353            }
354        }));
355        let listener = black_box(event.listen_with_note(|| {
356            dead_fn();
357            || {
358                dead_fn();
359                String::new()
360            }
361        }));
362
363        let _ = black_box(timeout(Duration::from_millis(10), listener)).await;
364    }
365
366    /// `EventListener::wait` has to actually block until another thread notifies the event.
367    ///
368    /// The notification is sent only after a delay, so the waiter is registered and parked first —
369    /// this exercises the parking path rather than the already-notified fast path. The assertions
370    /// are written so that a `wait()` which returned early (or did nothing at all) fails rather
371    /// than silently passing, and a `wait()` which never woke up would hang the test.
372    #[test]
373    fn wait_blocks_until_notified_by_another_thread() {
374        const DELAY: Duration = Duration::from_millis(300);
375        // Allow for timer granularity: assert on a slightly shorter span than we sleep for.
376        const MIN_BLOCKED: Duration = Duration::from_millis(200);
377
378        let event = Arc::new(Event::new(|| || "test event".to_string()));
379        let listener = event.listen();
380
381        let notified = Arc::new(AtomicBool::new(false));
382        let notifier = {
383            let event = event.clone();
384            let notified = notified.clone();
385            std::thread::spawn(move || {
386                std::thread::sleep(DELAY);
387                notified.store(true, Ordering::SeqCst);
388                event.notify(1);
389            })
390        };
391
392        let start = Instant::now();
393        listener.wait();
394        let blocked_for = start.elapsed();
395
396        assert!(
397            notified.load(Ordering::SeqCst),
398            "wait() returned before the notifying thread ran"
399        );
400        assert!(
401            blocked_for >= MIN_BLOCKED,
402            "wait() did not block; it returned after {blocked_for:?}"
403        );
404
405        notifier.join().unwrap();
406    }
407}