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}