Skip to main content

ntex_rt/
task.rs

1use std::{future::Future, pin::Pin, sync::Arc, sync::atomic::AtomicUsize, sync::atomic::Ordering};
2use std::{task::Context, task::Poll};
3
4// The Callbacks static holds a pointer to the global callbacks. It is protected by
5// the STATE static which determines whether `CBS` has been initialized yet.
6static mut CBS: Option<Arc<dyn CallbacksApi>> = None;
7
8static STATE: AtomicUsize = AtomicUsize::new(0);
9
10// There are three different states that we care about: the callback's
11// uninitialized, the callback's initializing (set_cbs's been called but
12// CBS hasn't actually been set yet), or the callbacks's active.
13const UNINITIALIZED: usize = 0;
14const INITIALIZING: usize = 1;
15const INITIALIZED: usize = 2;
16
17trait CallbacksApi {
18    fn before(&self) -> Option<*const ()>;
19    fn enter(&self, _: *const ()) -> *const ();
20    fn exit(&self, _: *const ());
21    fn after(&self, _: *const ());
22}
23
24#[allow(clippy::struct_field_names)]
25struct Callbacks<A, B, C, D> {
26    f_before: A,
27    f_enter: B,
28    f_exit: C,
29    f_after: D,
30}
31
32impl<A, B, C, D> CallbacksApi for Callbacks<A, B, C, D>
33where
34    A: Fn() -> Option<*const ()> + 'static,
35    B: Fn(*const ()) -> *const () + 'static,
36    C: Fn(*const ()) + 'static,
37    D: Fn(*const ()) + 'static,
38{
39    fn before(&self) -> Option<*const ()> {
40        (self.f_before)()
41    }
42    fn enter(&self, d: *const ()) -> *const () {
43        (self.f_enter)(d)
44    }
45    fn exit(&self, d: *const ()) {
46        (self.f_exit)(d);
47    }
48    fn after(&self, d: *const ()) {
49        (self.f_after)(d);
50    }
51}
52
53pub(crate) struct Data {
54    cb: &'static dyn CallbacksApi,
55    ptr: *const (),
56}
57
58impl Data {
59    #[allow(clippy::if_not_else)]
60    pub(crate) fn load() -> Option<Data> {
61        // `Acquire` pairs with the `Release` store in `set_cbs`, so `CBS` is visible
62        let cb = if STATE.load(Ordering::Acquire) != INITIALIZED {
63            None
64        } else {
65            #[allow(static_mut_refs)]
66            unsafe {
67                Some(CBS.as_ref().map(AsRef::as_ref).unwrap())
68            }
69        };
70
71        if let Some(cb) = cb
72            && let Some(ptr) = cb.before()
73        {
74            return Some(Data { cb, ptr });
75        }
76        None
77    }
78
79    pub(crate) fn run<F, R>(&mut self, f: F) -> R
80    where
81        F: FnOnce() -> R,
82    {
83        struct Exit(&'static dyn CallbacksApi, *const ());
84
85        impl Drop for Exit {
86            fn drop(&mut self) {
87                self.0.exit(self.1);
88            }
89        }
90
91        // `exit` also runs if `f` panics, so the context set by `enter` is restored
92        let _exit = Exit(self.cb, self.cb.enter(self.ptr));
93        f()
94    }
95}
96
97/// Wraps a future so that task callbacks, if registered, run around each poll.
98///
99/// Always returns the same future type, so spawning code is generated once
100/// per future rather than once per callback mode.
101pub(crate) fn wrap<F: Future>(fut: F) -> impl Future<Output = F::Output> {
102    let mut data = Data::load();
103    async move {
104        let mut f = std::pin::pin!(fut);
105        std::future::poll_fn(|cx| match data.as_mut() {
106            Some(data) => data.run(|| f.as_mut().poll(cx)),
107            None => f.as_mut().poll(cx),
108        })
109        .await
110    }
111}
112
113impl Drop for Data {
114    fn drop(&mut self) {
115        self.cb.after(self.ptr);
116    }
117}
118
119/// Spawns a local future and returns a [`SendableJoinHandle`] for its result.
120///
121/// The task is spawned immediately on the current runtime, using
122/// [`crate::spawn_detached`] and its registered task callbacks. The spawned
123/// future need not be `Send`, but its output must be. Only the returned handle
124/// may be moved to another thread; the task stays on its original runtime,
125/// which must keep being driven.
126///
127/// Dropping the handle detaches the task unless
128/// [`cancel_on_drop`](SendableJoinHandle::cancel_on_drop) is enabled.
129/// Use [`cancel`](SendableJoinHandle::cancel) to request cancellation explicitly.
130///
131/// Awaiting the handle yields the output or [`crate::JoinError`] if
132/// the task ends without one, for example on panic or runtime shutdown. Task
133/// panics follow the runtime's usual behavior.
134///
135/// # Panics
136///
137/// Panics outside a runtime context that supports [`crate::spawn_detached`].
138#[track_caller]
139pub fn spawn_sendable<F>(fut: F) -> SendableJoinHandle<F::Output>
140where
141    F: Future + 'static,
142    F::Output: Send,
143{
144    let (tx, rx) = oneshot::async_channel();
145    let (cancel, canceled) = oneshot::async_channel::<()>();
146    crate::spawn_detached(async move {
147        let output = {
148            let mut fut = std::pin::pin!(fut);
149            let mut canceled = Some(canceled);
150            std::future::poll_fn(|cx| {
151                if let Some(rx) = &mut canceled {
152                    match Pin::new(rx).poll(cx) {
153                        Poll::Ready(Ok(())) => return Poll::Ready(None),
154                        Poll::Ready(Err(_)) => canceled = None,
155                        Poll::Pending => {}
156                    }
157                }
158                fut.as_mut().poll(cx).map(Some)
159            })
160            .await
161        };
162        if let Some(output) = output {
163            let _ = tx.send(output);
164        }
165    });
166    SendableJoinHandle {
167        rx,
168        cancel: Some(cancel),
169        cancel_on_drop: false,
170    }
171}
172
173/// A sendable task handle returned by [`spawn_sendable()`].
174///
175/// The handle is `Send` when `T` is `Send`, but is not `Sync`. It may be moved
176/// and polled on another thread while the task stays on its original runtime.
177/// That runtime must keep being driven for the task to make progress.
178///
179/// Dropping the handle detaches the task unless
180/// [`cancel_on_drop`](Self::cancel_on_drop) is enabled. Awaiting it yields the
181/// output, or [`crate::JoinError`] if the task ends without one, including on
182/// panic or runtime shutdown.
183#[derive(Debug)]
184pub struct SendableJoinHandle<T> {
185    rx: oneshot::AsyncReceiver<T>,
186    cancel: Option<oneshot::Sender<()>>,
187    cancel_on_drop: bool,
188}
189
190impl<T> SendableJoinHandle<T> {
191    /// Requests cancellation when this handle is dropped instead of detaching the task.
192    ///
193    /// Cancellation follows [`cancel`](Self::cancel) and does not wait for cleanup.
194    /// A running poll is not interrupted.
195    #[must_use]
196    pub fn cancel_on_drop(mut self) -> Self {
197        self.cancel_on_drop = true;
198        self
199    }
200
201    /// Requests cancellation and discards the result without waiting for cleanup.
202    ///
203    /// The future is dropped on its runtime thread when the runtime next
204    /// processes the task, or during shutdown. A running poll is not interrupted.
205    /// This method may be called from another thread without a runtime context.
206    pub fn cancel(mut self) {
207        self.cancel_on_drop = true;
208    }
209
210    /// Returns `true` if the task completed or was closed without an output.
211    pub fn is_finished(&self) -> bool {
212        self.rx.has_message() || self.rx.is_closed()
213    }
214}
215
216impl<T> Drop for SendableJoinHandle<T> {
217    fn drop(&mut self) {
218        if self.cancel_on_drop
219            && let Some(cancel) = self.cancel.take()
220        {
221            let _ = cancel.send(());
222        }
223    }
224}
225
226impl<T> Future for SendableJoinHandle<T> {
227    type Output = Result<T, crate::JoinError>;
228
229    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
230        Pin::new(&mut self.rx)
231            .poll(cx)
232            .map(|result| result.map_err(|_| crate::JoinError))
233    }
234}
235
236/// Installs process-wide callbacks for instrumenting spawned tasks.
237///
238/// Callbacks apply when [`crate::spawn`], [`crate::spawn_detached`],
239/// [`spawn_sendable`], [`crate::rt::spawn`], or [`crate::rt::spawn_detached`] wraps a future, not
240/// retroactively to existing tasks. Direct [`crate::rt::Runtime`] spawning
241/// methods do not apply them.
242///
243/// - `f_before` runs synchronously when the future is wrapped. Returning `None`
244///   disables the remaining callbacks for that future.
245/// - `f_enter` receives the task's pointer before each poll and returns a pointer
246///   representing the previous context.
247/// - `f_exit` receives that previous-context pointer after the poll, including
248///   when polling unwinds.
249/// - `f_after` receives the task's original pointer when its callback state is
250///   dropped, including cancellation before the first poll.
251///
252/// Registration is permanent and first-wins. Does nothing if callbacks were
253/// already installed; use [`task_opt_callbacks`] to observe registration success.
254///
255/// # Safety
256///
257/// Pointers are opaque to the runtime. The caller must keep the state returned
258/// by `f_before` valid until `f_after`, and the state returned by `f_enter` valid
259/// until its matching `f_exit`. Callbacks must safely handle concurrent use by
260/// different runtime threads and any nested task activity they trigger.
261pub unsafe fn task_callbacks<FBefore, FEnter, FExit, FAfter>(
262    f_before: FBefore,
263    f_enter: FEnter,
264    f_exit: FExit,
265    f_after: FAfter,
266) where
267    FBefore: Fn() -> Option<*const ()> + 'static + Sync,
268    FEnter: Fn(*const ()) -> *const () + 'static + Sync,
269    FExit: Fn(*const ()) + 'static + Sync,
270    FAfter: Fn(*const ()) + 'static + Sync,
271{
272    let new = Arc::new(Callbacks {
273        f_before,
274        f_enter,
275        f_exit,
276        f_after,
277    });
278    let _ = set_cbs(new);
279}
280
281/// Attempts to install process-wide task callbacks.
282///
283/// Uses the callback lifecycle described by [`task_callbacks`]. Returns `true`
284/// if this call installs the callbacks, or `false` if another registration has
285/// already won. Installed callbacks cannot be replaced or removed.
286///
287/// # Safety
288///
289/// The pointer lifetimes and callback thread-safety requirements of
290/// [`task_callbacks`] apply.
291pub unsafe fn task_opt_callbacks<FBefore, FEnter, FExit, FAfter>(
292    f_before: FBefore,
293    f_enter: FEnter,
294    f_exit: FExit,
295    f_after: FAfter,
296) -> bool
297where
298    FBefore: Fn() -> Option<*const ()> + Sync + 'static,
299    FEnter: Fn(*const ()) -> *const () + Sync + 'static,
300    FExit: Fn(*const ()) + Sync + 'static,
301    FAfter: Fn(*const ()) + Sync + 'static,
302{
303    let new = Arc::new(Callbacks {
304        f_before,
305        f_enter,
306        f_exit,
307        f_after,
308    });
309    set_cbs(new).is_ok()
310}
311
312fn set_cbs(cbs: Arc<dyn CallbacksApi>) -> Result<(), ()> {
313    match STATE.compare_exchange(
314        UNINITIALIZED,
315        INITIALIZING,
316        Ordering::Acquire,
317        Ordering::Relaxed,
318    ) {
319        Ok(UNINITIALIZED) => {
320            unsafe {
321                CBS = Some(cbs);
322            }
323            STATE.store(INITIALIZED, Ordering::Release);
324            Ok(())
325        }
326        Err(INITIALIZING) => {
327            while STATE.load(Ordering::Relaxed) == INITIALIZING {
328                std::hint::spin_loop();
329            }
330            Err(())
331        }
332        _ => Err(()),
333    }
334}
335
336#[cfg(test)]
337#[path = "../tests/unit/task.rs"]
338mod tests;