Skip to main content

ntex_rt/
task.rs

1use std::{future::Future, sync::Arc, sync::atomic::AtomicUsize, sync::atomic::Ordering};
2
3// The Callbacks static holds a pointer to the global callbacks. It is protected by
4// the STATE static which determines whether `CBS` has been initialized yet.
5static mut CBS: Option<Arc<dyn CallbacksApi>> = None;
6
7static STATE: AtomicUsize = AtomicUsize::new(0);
8
9// There are three different states that we care about: the callback's
10// uninitialized, the callback's initializing (set_cbs's been called but
11// CBS hasn't actually been set yet), or the callbacks's active.
12const UNINITIALIZED: usize = 0;
13const INITIALIZING: usize = 1;
14const INITIALIZED: usize = 2;
15
16trait CallbacksApi {
17    fn before(&self) -> Option<*const ()>;
18    fn enter(&self, _: *const ()) -> *const ();
19    fn exit(&self, _: *const ());
20    fn after(&self, _: *const ());
21}
22
23#[allow(clippy::struct_field_names)]
24struct Callbacks<A, B, C, D> {
25    f_before: A,
26    f_enter: B,
27    f_exit: C,
28    f_after: D,
29}
30
31impl<A, B, C, D> CallbacksApi for Callbacks<A, B, C, D>
32where
33    A: Fn() -> Option<*const ()> + 'static,
34    B: Fn(*const ()) -> *const () + 'static,
35    C: Fn(*const ()) + 'static,
36    D: Fn(*const ()) + 'static,
37{
38    fn before(&self) -> Option<*const ()> {
39        (self.f_before)()
40    }
41    fn enter(&self, d: *const ()) -> *const () {
42        (self.f_enter)(d)
43    }
44    fn exit(&self, d: *const ()) {
45        (self.f_exit)(d);
46    }
47    fn after(&self, d: *const ()) {
48        (self.f_after)(d);
49    }
50}
51
52pub(crate) struct Data {
53    cb: &'static dyn CallbacksApi,
54    ptr: *const (),
55}
56
57impl Data {
58    #[allow(clippy::if_not_else)]
59    pub(crate) fn load() -> Option<Data> {
60        // `Acquire` pairs with the `Release` store in `set_cbs`, so `CBS` is visible
61        let cb = if STATE.load(Ordering::Acquire) != INITIALIZED {
62            None
63        } else {
64            #[allow(static_mut_refs)]
65            unsafe {
66                Some(CBS.as_ref().map(AsRef::as_ref).unwrap())
67            }
68        };
69
70        if let Some(cb) = cb
71            && let Some(ptr) = cb.before()
72        {
73            return Some(Data { cb, ptr });
74        }
75        None
76    }
77
78    pub(crate) fn run<F, R>(&mut self, f: F) -> R
79    where
80        F: FnOnce() -> R,
81    {
82        struct Exit(&'static dyn CallbacksApi, *const ());
83
84        impl Drop for Exit {
85            fn drop(&mut self) {
86                self.0.exit(self.1);
87            }
88        }
89
90        // `exit` also runs if `f` panics, so the context set by `enter` is restored
91        let _exit = Exit(self.cb, self.cb.enter(self.ptr));
92        f()
93    }
94}
95
96/// Wraps a future so that task callbacks, if registered, run around each poll.
97///
98/// Always returns the same future type, so spawning code is generated once
99/// per future rather than once per callback mode.
100pub(crate) fn wrap<F: Future>(fut: F) -> impl Future<Output = F::Output> {
101    let mut data = Data::load();
102    async move {
103        let mut f = std::pin::pin!(fut);
104        std::future::poll_fn(|cx| match data.as_mut() {
105            Some(data) => data.run(|| f.as_mut().poll(cx)),
106            None => f.as_mut().poll(cx),
107        })
108        .await
109    }
110}
111
112#[cfg(any(feature = "tokio", feature = "compio"))]
113pub(crate) use remote::RemoteJoin;
114
115#[cfg(any(feature = "tokio", feature = "compio"))]
116mod remote {
117    use std::sync::atomic::{AtomicBool, Ordering};
118    use std::task::{Context, Poll};
119    use std::{future::Future, future::poll_fn, pin::Pin, pin::pin, sync::Arc};
120
121    use async_channel::Sender;
122    use atomic_waker::AtomicWaker;
123
124    use crate::arbiter::ArbiterCommand;
125
126    #[derive(Debug, Default)]
127    struct Cancel {
128        cancelled: AtomicBool,
129        waker: AtomicWaker,
130    }
131
132    /// A task spawned on an arbiter from any thread, cancelling it drops the
133    /// future on the arbiter thread.
134    #[derive(Debug)]
135    pub(crate) struct RemoteJoin<T> {
136        rx: oneshot::AsyncReceiver<T>,
137        cancel: Arc<Cancel>,
138    }
139
140    impl<T: Send + 'static> RemoteJoin<T> {
141        pub(crate) fn spawn<F>(sender: &Sender<ArbiterCommand>, future: F) -> Self
142        where
143            F: Future<Output = T> + Send + 'static,
144        {
145            let (tx, rx) = oneshot::async_channel();
146            let cancel = Arc::new(Cancel::default());
147            let state = cancel.clone();
148            let _ = sender.try_send(ArbiterCommand::Execute(Box::pin(async move {
149                let mut fut = pin!(future);
150                let res = poll_fn(|cx| {
151                    state.waker.register(cx.waker());
152                    if state.cancelled.load(Ordering::Acquire) {
153                        Poll::Ready(None)
154                    } else {
155                        fut.as_mut().poll(cx).map(Some)
156                    }
157                })
158                .await;
159                if let Some(res) = res {
160                    let _ = tx.send(res);
161                }
162            })));
163            Self { rx, cancel }
164        }
165    }
166
167    impl<T> RemoteJoin<T> {
168        /// Cancels the task, a task that did not start never runs.
169        pub(crate) fn cancel(self) {
170            self.cancel.cancelled.store(true, Ordering::Release);
171            self.cancel.waker.wake();
172        }
173
174        pub(crate) fn is_finished(&self) -> bool {
175            self.rx.is_closed()
176        }
177    }
178
179    impl<T> Future for RemoteJoin<T> {
180        type Output = Result<T, oneshot::RecvError>;
181
182        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
183            Pin::new(&mut self.rx).poll(cx)
184        }
185    }
186}
187
188impl Drop for Data {
189    fn drop(&mut self) {
190        self.cb.after(self.ptr);
191    }
192}
193
194/// # Safety
195///
196/// The user must ensure that the pointer returned by `before` has a `'static` lifetime.
197/// This pointer will be owned by the spawned task for the duration of that task, and
198/// ownership will be returned to the user at the end of the task via `after`.
199/// The pointer remains opaque to the runtime.
200///
201/// Does nothing if task callbacks have already been set, use
202/// [`task_opt_callbacks`] to check whether they were set.
203pub unsafe fn task_callbacks<FBefore, FEnter, FExit, FAfter>(
204    f_before: FBefore,
205    f_enter: FEnter,
206    f_exit: FExit,
207    f_after: FAfter,
208) where
209    FBefore: Fn() -> Option<*const ()> + 'static + Sync,
210    FEnter: Fn(*const ()) -> *const () + 'static + Sync,
211    FExit: Fn(*const ()) + 'static + Sync,
212    FAfter: Fn(*const ()) + 'static + Sync,
213{
214    let new = Arc::new(Callbacks {
215        f_before,
216        f_enter,
217        f_exit,
218        f_after,
219    });
220    let _ = set_cbs(new);
221}
222
223/// # Safety
224///
225/// The user must ensure that the pointer returned by `before` has a `'static` lifetime.
226/// This pointer will be owned by the spawned task for the duration of that task, and
227/// ownership will be returned to the user at the end of the task via `after`.
228/// The pointer remains opaque to the runtime.
229///
230/// Returns false if task callbacks have already been set.
231pub unsafe fn task_opt_callbacks<FBefore, FEnter, FExit, FAfter>(
232    f_before: FBefore,
233    f_enter: FEnter,
234    f_exit: FExit,
235    f_after: FAfter,
236) -> bool
237where
238    FBefore: Fn() -> Option<*const ()> + Sync + 'static,
239    FEnter: Fn(*const ()) -> *const () + Sync + 'static,
240    FExit: Fn(*const ()) + Sync + 'static,
241    FAfter: Fn(*const ()) + Sync + 'static,
242{
243    let new = Arc::new(Callbacks {
244        f_before,
245        f_enter,
246        f_exit,
247        f_after,
248    });
249    set_cbs(new).is_ok()
250}
251
252fn set_cbs(cbs: Arc<dyn CallbacksApi>) -> Result<(), ()> {
253    match STATE.compare_exchange(
254        UNINITIALIZED,
255        INITIALIZING,
256        Ordering::Acquire,
257        Ordering::Relaxed,
258    ) {
259        Ok(UNINITIALIZED) => {
260            unsafe {
261                CBS = Some(cbs);
262            }
263            STATE.store(INITIALIZED, Ordering::Release);
264            Ok(())
265        }
266        Err(INITIALIZING) => {
267            while STATE.load(Ordering::Relaxed) == INITIALIZING {
268                std::hint::spin_loop();
269            }
270            Err(())
271        }
272        _ => Err(()),
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn load_callbacks_set_by_other_thread() {
282        let hnd = std::thread::spawn(|| unsafe {
283            task_opt_callbacks(|| Some(std::ptr::null()), |p| p, |_| {}, |_| {})
284        });
285        // observes callbacks set by another thread without synchronization
286        while Data::load().is_none() {
287            std::hint::spin_loop();
288        }
289        assert!(hnd.join().unwrap());
290    }
291
292    #[test]
293    fn exit_runs_when_poll_panics() {
294        use std::{cell::Cell, panic, pin::pin, task::Context, task::Waker};
295
296        // callbacks can be set once per process, so check them in a child process
297        const CHILD: &str = "NTEX_RT_TASK_EXIT_CHILD";
298        if std::env::var_os(CHILD).is_none() {
299            let out = std::process::Command::new(std::env::current_exe().unwrap())
300                .args(["--exact", "task::tests::exit_runs_when_poll_panics"])
301                .env(CHILD, "1")
302                .output()
303                .unwrap();
304            assert!(
305                out.status.success(),
306                "{}",
307                String::from_utf8_lossy(&out.stdout)
308            );
309            return;
310        }
311
312        thread_local! {
313            static DEPTH: Cell<isize> = const { Cell::new(0) };
314            static AFTER: Cell<usize> = const { Cell::new(0) };
315        }
316        assert!(unsafe {
317            task_opt_callbacks(
318                || Some(std::ptr::null()),
319                |p| {
320                    DEPTH.set(DEPTH.get() + 1);
321                    p
322                },
323                |_| DEPTH.set(DEPTH.get() - 1),
324                |_| AFTER.set(AFTER.get() + 1),
325            )
326        });
327
328        let mut fut = Box::pin(wrap(async { panic!("poll panic") }));
329        let res = panic::catch_unwind(panic::AssertUnwindSafe(|| {
330            let _ = fut.as_mut().poll(&mut Context::from_waker(Waker::noop()));
331        }));
332        assert!(res.is_err());
333        assert_eq!(DEPTH.get(), 0);
334        drop(fut);
335        assert_eq!(AFTER.get(), 1);
336
337        let mut fut = pin!(wrap(async { 1 }));
338        let res = fut.as_mut().poll(&mut Context::from_waker(Waker::noop()));
339        assert_eq!(res, std::task::Poll::Ready(1));
340        assert_eq!(DEPTH.get(), 0);
341    }
342
343    #[test]
344    fn callbacks_set_once_and_wrap_spawned_tasks() {
345        use std::{cell::Cell, sync::atomic::AtomicUsize};
346
347        use crate::{System, testing::TestRunner};
348
349        const CHILD: &str = "NTEX_RT_TASK_CALLBACKS_CHILD";
350        static DATA: u8 = 0;
351        static CTX: u8 = 0;
352        static BEFORE: AtomicUsize = AtomicUsize::new(0);
353        static AFTER: AtomicUsize = AtomicUsize::new(0);
354        thread_local! {
355            static TRACK: Cell<bool> = const { Cell::new(false) };
356            static INSIDE: Cell<bool> = const { Cell::new(false) };
357        }
358        fn data() -> *const () {
359            (&raw const DATA).cast()
360        }
361        fn ctx() -> *const () {
362            (&raw const CTX).cast()
363        }
364
365        // callbacks can be set once per process, so check them in a child process
366        if std::env::var_os(CHILD).is_none() {
367            let out = std::process::Command::new(std::env::current_exe().unwrap())
368                .args([
369                    "--exact",
370                    "task::tests::callbacks_set_once_and_wrap_spawned_tasks",
371                ])
372                .env(CHILD, "1")
373                .output()
374                .unwrap();
375            assert!(
376                out.status.success(),
377                "{}",
378                String::from_utf8_lossy(&out.stdout)
379            );
380            return;
381        }
382
383        unsafe {
384            task_callbacks(
385                || {
386                    TRACK.get().then(|| {
387                        BEFORE.fetch_add(1, Ordering::Relaxed);
388                        data()
389                    })
390                },
391                |p| {
392                    assert_eq!(p, data());
393                    INSIDE.set(true);
394                    ctx()
395                },
396                |p| {
397                    // receives the value returned by `enter`
398                    assert_eq!(p, ctx());
399                    INSIDE.set(false);
400                },
401                |p| {
402                    assert_eq!(p, data());
403                    AFTER.fetch_add(1, Ordering::Relaxed);
404                },
405            );
406        }
407        // callbacks are set once, later registrations are ignored
408        unsafe {
409            task_callbacks(
410                || unreachable!(),
411                |_| unreachable!(),
412                |_| unreachable!(),
413                |_| unreachable!(),
414            );
415            assert!(!task_opt_callbacks(
416                || unreachable!(),
417                |_| unreachable!(),
418                |_| unreachable!(),
419                |_| unreachable!(),
420            ));
421        }
422
423        System::new("callbacks", TestRunner).block_on(async {
424            TRACK.set(true);
425            let hnd = crate::spawn(async { INSIDE.get() });
426            TRACK.set(false);
427            assert_eq!(BEFORE.load(Ordering::Relaxed), 1);
428            // polled with the context set by `enter`
429            assert!(hnd.await.unwrap());
430            assert!(!INSIDE.get());
431            assert_eq!(AFTER.load(Ordering::Relaxed), 1);
432
433            // `before` returned `None`, the task runs without callbacks
434            assert!(!crate::spawn(async { INSIDE.get() }).await.unwrap());
435            assert_eq!(BEFORE.load(Ordering::Relaxed), 1);
436            assert_eq!(AFTER.load(Ordering::Relaxed), 1);
437        });
438    }
439}