ntex_rt/task.rs
1use std::{cell::Cell, future::Future, pin::Pin, task::Context, task::Poll};
2
3thread_local!(
4 // callbacks created by the current system's factory, set together with the current system
5 static CALLBACKS: Cell<Option<TaskCallbacks>> = const { Cell::new(None) };
6);
7
8/// Callbacks for instrumenting spawned tasks.
9///
10/// Configure a factory returning them with
11/// [`Builder::task_callbacks`](crate::Builder::task_callbacks) or the
12/// `task_callbacks` argument of `#[ntex::main]` and `#[ntex::test]`. The factory
13/// is called once on every thread of the system, when the thread joins it.
14/// They apply when [`crate::spawn`], [`crate::spawn_detached`],
15/// [`spawn_sendable`], [`crate::rt::spawn`], or [`crate::rt::spawn_detached`]
16/// wraps a future on a thread of the configured system, including its
17/// arbiters. They do not apply retroactively, to direct
18/// [`crate::rt::Runtime`] spawning methods, or on threads without a system.
19///
20/// - `before` runs synchronously when the future is wrapped. Returning `None`
21/// disables the remaining callbacks for that future.
22/// - `enter` receives the task's pointer before each poll and returns a pointer
23/// representing the previous context.
24/// - `exit` receives that previous-context pointer after the poll, including
25/// when polling unwinds.
26/// - `after` receives the task's original pointer when its callback state is
27/// dropped, including cancellation before the first poll.
28///
29/// Pointers are opaque to the runtime, which never dereferences them. Keep the
30/// state returned by `before` valid until `after`, and the state returned by
31/// `enter` valid until its matching `exit`. Callbacks of a thread run only on
32/// that thread and may be called while other tasks are spawned.
33#[derive(Copy, Clone, Debug)]
34pub struct TaskCallbacks {
35 /// Called when a future is wrapped, returns the task's pointer.
36 pub before: fn() -> Option<*const ()>,
37 /// Called before each poll, returns the previous context.
38 pub enter: fn(*const ()) -> *const (),
39 /// Called after each poll with the value returned by `enter`.
40 pub exit: fn(*const ()),
41 /// Called once with the task's pointer when its callback state is dropped.
42 pub after: fn(*const ()),
43}
44
45/// Installs the current system's callbacks on this thread.
46pub(crate) fn set_callbacks(cbs: Option<TaskCallbacks>) {
47 CALLBACKS.set(cbs);
48}
49
50pub(crate) struct Data {
51 cbs: TaskCallbacks,
52 ptr: *const (),
53}
54
55impl Data {
56 pub(crate) fn load() -> Option<Data> {
57 let cbs = CALLBACKS.get()?;
58 (cbs.before)().map(|ptr| Data { cbs, ptr })
59 }
60
61 pub(crate) fn run<F, R>(&mut self, f: F) -> R
62 where
63 F: FnOnce() -> R,
64 {
65 struct Exit(fn(*const ()), *const ());
66
67 impl Drop for Exit {
68 fn drop(&mut self) {
69 (self.0)(self.1);
70 }
71 }
72
73 // `exit` also runs if `f` panics, so the context set by `enter` is restored
74 let _exit = Exit(self.cbs.exit, (self.cbs.enter)(self.ptr));
75 f()
76 }
77}
78
79/// Wraps a future so that task callbacks, if registered, run around each poll.
80///
81/// Always returns the same future type, so spawning code is generated once
82/// per future rather than once per callback mode.
83pub(crate) fn wrap<F: Future>(fut: F) -> impl Future<Output = F::Output> {
84 let mut data = Data::load();
85 async move {
86 let mut f = std::pin::pin!(fut);
87 std::future::poll_fn(|cx| match data.as_mut() {
88 Some(data) => data.run(|| f.as_mut().poll(cx)),
89 None => f.as_mut().poll(cx),
90 })
91 .await
92 }
93}
94
95impl Drop for Data {
96 fn drop(&mut self) {
97 (self.cbs.after)(self.ptr);
98 }
99}
100
101/// Spawns a local future and returns a [`SendableJoinHandle`] for its result.
102///
103/// The task is spawned immediately on the current runtime, using
104/// [`crate::spawn_detached`] and the current system's [`TaskCallbacks`]. The spawned
105/// future need not be `Send`, but its output must be. Only the returned handle
106/// may be moved to another thread; the task stays on its original runtime,
107/// which must keep being driven.
108///
109/// Dropping the handle detaches the task unless
110/// [`cancel_on_drop`](SendableJoinHandle::cancel_on_drop) is enabled.
111/// Use [`cancel`](SendableJoinHandle::cancel) to request cancellation explicitly.
112///
113/// Awaiting the handle yields the output or [`crate::JoinError`] if
114/// the task ends without one, for example on panic or runtime shutdown. Task
115/// panics follow the runtime's usual behavior.
116///
117/// # Panics
118///
119/// Panics outside a runtime context that supports [`crate::spawn_detached`].
120#[track_caller]
121pub fn spawn_sendable<F>(fut: F) -> SendableJoinHandle<F::Output>
122where
123 F: Future + 'static,
124 F::Output: Send,
125{
126 let (tx, rx) = oneshot::async_channel();
127 let (cancel, canceled) = oneshot::async_channel::<()>();
128 crate::spawn_detached(async move {
129 let output = {
130 let mut fut = std::pin::pin!(fut);
131 let mut canceled = Some(canceled);
132 std::future::poll_fn(|cx| {
133 if let Some(rx) = &mut canceled {
134 match Pin::new(rx).poll(cx) {
135 Poll::Ready(Ok(())) => return Poll::Ready(None),
136 Poll::Ready(Err(_)) => canceled = None,
137 Poll::Pending => {}
138 }
139 }
140 fut.as_mut().poll(cx).map(Some)
141 })
142 .await
143 };
144 if let Some(output) = output {
145 let _ = tx.send(output);
146 }
147 });
148 SendableJoinHandle {
149 rx,
150 cancel: Some(cancel),
151 cancel_on_drop: false,
152 }
153}
154
155/// A sendable task handle returned by [`spawn_sendable()`].
156///
157/// The handle is `Send` when `T` is `Send`, but is not `Sync`. It may be moved
158/// and polled on another thread while the task stays on its original runtime.
159/// That runtime must keep being driven for the task to make progress.
160///
161/// Dropping the handle detaches the task unless
162/// [`cancel_on_drop`](Self::cancel_on_drop) is enabled. Awaiting it yields the
163/// output, or [`crate::JoinError`] if the task ends without one, including on
164/// panic or runtime shutdown.
165#[derive(Debug)]
166pub struct SendableJoinHandle<T> {
167 rx: oneshot::AsyncReceiver<T>,
168 cancel: Option<oneshot::Sender<()>>,
169 cancel_on_drop: bool,
170}
171
172impl<T> SendableJoinHandle<T> {
173 /// Requests cancellation when this handle is dropped instead of detaching the task.
174 ///
175 /// Cancellation follows [`cancel`](Self::cancel) and does not wait for cleanup.
176 /// A running poll is not interrupted.
177 #[must_use]
178 pub fn cancel_on_drop(mut self) -> Self {
179 self.cancel_on_drop = true;
180 self
181 }
182
183 /// Requests cancellation and discards the result without waiting for cleanup.
184 ///
185 /// The future is dropped on its runtime thread when the runtime next
186 /// processes the task, or during shutdown. A running poll is not interrupted.
187 /// This method may be called from another thread without a runtime context.
188 pub fn cancel(mut self) {
189 self.cancel_on_drop = true;
190 }
191
192 /// Returns `true` if the task completed or was closed without an output.
193 pub fn is_finished(&self) -> bool {
194 self.rx.has_message() || self.rx.is_closed()
195 }
196}
197
198impl<T> Drop for SendableJoinHandle<T> {
199 fn drop(&mut self) {
200 if self.cancel_on_drop
201 && let Some(cancel) = self.cancel.take()
202 {
203 let _ = cancel.send(());
204 }
205 }
206}
207
208impl<T> Future for SendableJoinHandle<T> {
209 type Output = Result<T, crate::JoinError>;
210
211 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
212 Pin::new(&mut self.rx)
213 .poll(cx)
214 .map(|result| result.map_err(|_| crate::JoinError))
215 }
216}
217
218#[cfg(test)]
219#[path = "../tests/unit/task.rs"]
220mod tests;