Skip to main content

nuclei/
task.rs

1//! Tasks: spawning (named, or on a worker the caller names, or neither),
2//! their handles and ids, cancellation from anywhere, and yielding to the
3//! other tasks of the shard.
4
5use core::future::Future;
6use core::pin::Pin;
7use core::task::{Context, Poll};
8use std::io;
9
10pub use crate::join_set::JoinSet;
11
12/// Cooperative scheduling: the budget of operations each poll of a task may
13/// complete before it yields to the other tasks of its shard. IO that
14/// completes spends one, and so do the receives, acquires and locks of
15/// [`sync`](crate::sync) that complete without queueing (see its module
16/// docs).
17pub mod coop {
18    pub use crate::coop::{
19        BUDGET, Cooperative, Unconstrained, consume_budget, cooperative, has_budget_remaining,
20        unconstrained,
21    };
22}
23pub use crate::rt::task::{AbortHandle, FallibleTask, Id, Join, JoinError, Task};
24pub use crate::runtime::{spawn, spawn_blocking, spawn_local};
25pub use crate::task_local::{AccessError, LocalKey, TaskLocalFuture};
26
27/// The id of the task this code runs in.
28///
29/// # Panics
30///
31/// Outside a task: in `block_on`'s own future, or on a blocking-pool thread.
32/// [`try_id`] answers `None` there instead.
33#[expect(
34    clippy::expect_used,
35    reason = "documented under `# Panics`; `try_id` answers `None` instead"
36)]
37pub fn id() -> Id {
38    try_id().expect("`nuclei::task::id` called outside a task")
39}
40
41/// The id of the task this code runs in, `None` outside a task (in
42/// `block_on`'s own future, or on a blocking-pool thread).
43pub fn try_id() -> Option<Id> {
44    crate::rt::task::current_id()
45}
46
47/// Gives the other tasks of this shard a turn: the task goes to the back of
48/// its shard's run queue and runs again after the tasks queued before it. In
49/// `block_on`'s own future it lets the shard run its queue once.
50pub async fn yield_now() {
51    YieldNow { yielded: false }.await
52}
53
54/// Pending once, waking itself: a task woken during its own poll is queued
55/// behind the others.
56struct YieldNow {
57    yielded: bool,
58}
59
60impl Future for YieldNow {
61    type Output = ();
62
63    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
64        if self.yielded {
65            return Poll::Ready(());
66        }
67        self.yielded = true;
68        cx.waker().wake_by_ref();
69        Poll::Pending
70    }
71}
72
73/// Spawns tasks with options: a name, kept with the task's id while the
74/// task lives and shown to the runtime's task hooks
75/// ([`TaskMeta::name`](crate::runtime::TaskMeta::name)), and the worker to
76/// place it on. Every spawn answers an error where the free functions
77/// panic.
78///
79/// ```
80/// # nuclei::block_on(async {
81/// let task = nuclei::task::Builder::new().name("fetch").spawn(async { 7 })?;
82/// assert_eq!(task.await, 7);
83/// # std::io::Result::Ok(())
84/// # }).unwrap();
85/// ```
86#[derive(Debug, Default, Clone, Copy)]
87pub struct Builder<'a> {
88    name: Option<&'a str>,
89    worker: Option<usize>,
90}
91
92std::thread_local! {
93    /// The name a builder's spawn on this thread gives its task, until the
94    /// spawn names it (before the hooks see the task) or the builder does.
95    static NAMING: core::cell::RefCell<Option<std::sync::Arc<str>>> =
96        const { core::cell::RefCell::new(None) };
97}
98
99/// Names the task `id`, just made by a builder's spawn on this thread, with
100/// the builder's name, before any hook of its runtime sees it.
101pub(crate) fn name_now(id: Id) {
102    if let Some(name) = NAMING
103        .try_with(|naming| naming.borrow_mut().take())
104        .ok()
105        .flatten()
106    {
107        crate::rt::task::names::set(id, &name);
108    }
109}
110
111impl<'a> Builder<'a> {
112    /// A builder with no option set.
113    pub fn new() -> Self {
114        Self {
115            name: None,
116            worker: None,
117        }
118    }
119
120    /// Names the task.
121    pub fn name(self, name: &'a str) -> Self {
122        Self {
123            name: Some(name),
124            ..self
125        }
126    }
127
128    /// Places the task on worker `worker` of its runtime, where it stays, as
129    /// [`RuntimeHandle::spawn_on_worker`](crate::runtime::RuntimeHandle::spawn_on_worker)
130    /// does. A [`spawn_local`](Self::spawn_local) then needs to run on that
131    /// worker, and a [`spawn_blocking`](Self::spawn_blocking) is refused: the
132    /// blocking pool runs no worker.
133    pub fn worker(self, worker: usize) -> Self {
134        Self {
135            worker: Some(worker),
136            ..self
137        }
138    }
139
140    /// Spawns as [`spawn`] does, on the named worker if one is set.
141    ///
142    /// # Errors
143    ///
144    /// When the default runtime or this thread's shard cannot start, and
145    /// `InvalidInput` when the named worker does not exist or takes no new
146    /// task.
147    pub fn spawn<F>(self, future: F) -> io::Result<Task<F::Output>>
148    where
149        F: Future + Send + 'static,
150        F::Output: Send + 'static,
151    {
152        self.named(|| {
153            let inner = crate::rt::runtime::try_current()?;
154            crate::rt::runtime::try_spawn(&inner, self.worker, future)
155        })
156    }
157
158    /// Spawns as [`spawn_local`] does.
159    ///
160    /// # Errors
161    ///
162    /// When this thread's shard cannot start, and `InvalidInput` when a
163    /// worker is named that this thread does not run: a future that is not
164    /// `Send` cannot go to another thread.
165    pub fn spawn_local<F>(self, future: F) -> io::Result<Task<F::Output>>
166    where
167        F: Future + 'static,
168        F::Output: 'static,
169    {
170        self.named(|| crate::rt::runtime::try_spawn_local(self.worker, future))
171    }
172
173    /// Runs `f` on the blocking pool as [`spawn_blocking`] does.
174    ///
175    /// # Errors
176    ///
177    /// When the default runtime cannot start, and `InvalidInput` when a
178    /// worker is named.
179    pub fn spawn_blocking<F, T>(self, f: F) -> io::Result<Task<T>>
180    where
181        F: FnOnce() -> T + Send + 'static,
182        T: Send + 'static,
183    {
184        self.named(|| crate::rt::runtime::try_spawn_blocking(self.worker, f))
185    }
186
187    /// Spawns on the runtime `handle` refers to, as
188    /// [`RuntimeHandle::spawn`](crate::runtime::RuntimeHandle::spawn) does,
189    /// on the named worker if one is set.
190    ///
191    /// # Errors
192    ///
193    /// When this thread's shard cannot start (the runtime has no worker
194    /// taking tasks), and `InvalidInput` when the named worker does not exist
195    /// or takes no new task.
196    pub fn spawn_on<F>(
197        self,
198        future: F,
199        handle: &crate::runtime::RuntimeHandle,
200    ) -> io::Result<Task<F::Output>>
201    where
202        F: Future + Send + 'static,
203        F::Output: Send + 'static,
204    {
205        self.named(|| crate::rt::runtime::try_spawn(handle.inner(), self.worker, future))
206    }
207
208    /// Runs `spawn` with the builder's name waiting for the task it makes,
209    /// then names the task if no hook's spawn did; the name is in place
210    /// while this call holds the task's handle, so before the task can be
211    /// destroyed.
212    fn named<T>(self, spawn: impl FnOnce() -> io::Result<Task<T>>) -> io::Result<Task<T>> {
213        let Some(name) = self.name else {
214            return spawn();
215        };
216        let name: std::sync::Arc<str> = std::sync::Arc::from(name);
217        let _ = NAMING.try_with(|naming| *naming.borrow_mut() = Some(std::sync::Arc::clone(&name)));
218        let spawned = spawn();
219        let waiting = NAMING
220            .try_with(|naming| naming.borrow_mut().take())
221            .ok()
222            .flatten();
223        if let (Ok(task), Some(name)) = (&spawned, waiting) {
224            crate::rt::task::names::set(task.id(), &name);
225        }
226        spawned
227    }
228}
229
230#[cfg(all(test, not(loom)))]
231mod tests {
232    use super::*;
233    use crate::rt::task::names;
234
235    /// A name is readable while its task lives and gone once the task is:
236    /// the table holds nothing for tasks that ended, however they ended.
237    #[test]
238    fn names_live_exactly_as_long_as_their_tasks() {
239        crate::block_on(async {
240            let done = Builder::new().name("done").spawn(async { 1 }).unwrap();
241            let id = done.id();
242            assert_eq!(names::get(id).as_deref(), Some("done"));
243            assert_eq!(done.await, 1);
244
245            let cancelled = Builder::new()
246                .name("cancelled")
247                .spawn(core::future::pending::<()>())
248                .unwrap();
249            cancelled.abort();
250            assert!(cancelled.join().await.unwrap_err().is_cancelled());
251
252            let blocking = Builder::new()
253                .name("blocking")
254                .spawn_blocking(|| 2)
255                .unwrap();
256            assert_eq!(blocking.await, 2);
257
258            let unnamed = Builder::new().spawn(async { 3 }).unwrap();
259            assert_eq!(names::get(unnamed.id()), None);
260            assert_eq!(unnamed.await, 3);
261
262            // Destroyed tasks have dropped their names (their last reference
263            // may go on a worker a moment after the handle).
264            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
265            while names::count() != 0 && std::time::Instant::now() < deadline {
266                yield_now().await;
267            }
268            assert_eq!(names::count(), 0, "a destroyed task kept its name");
269            assert_eq!(names::get(id), None);
270        });
271    }
272}