Skip to main content

nuclei/
task_local.rs

1//! Task-local values: [`task_local!`](crate::task_local) declares a key, and
2//! [`LocalKey::scope`] runs a future with a value set for that key in every
3//! one of its polls, wherever the task runs. The value lives in the future,
4//! not in the thread: it is swapped into the key's thread-local slot for each
5//! poll and taken back after it, so tasks of one shard never see each other's
6//! values.
7
8use core::cell::RefCell;
9use core::fmt;
10use core::future::Future;
11use core::marker::PhantomPinned;
12use core::pin::Pin;
13use core::task::{Context, Poll};
14
15/// Declares task-local keys, each a [`LocalKey`].
16///
17/// ```
18/// nuclei::task_local! {
19///     static REQUEST_ID: u64;
20/// }
21///
22/// # nuclei::block_on(async {
23/// REQUEST_ID
24///     .scope(7, async {
25///         assert_eq!(REQUEST_ID.get(), 7);
26///     })
27///     .await;
28/// # });
29/// ```
30#[macro_export]
31macro_rules! task_local {
32    () => {};
33    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty; $($rest:tt)*) => {
34        $crate::__task_local_key!($(#[$attr])* $vis $name, $t);
35        $crate::task_local!($($rest)*);
36    };
37    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty) => {
38        $crate::__task_local_key!($(#[$attr])* $vis $name, $t);
39    };
40}
41
42#[doc(hidden)]
43#[macro_export]
44macro_rules! __task_local_key {
45    ($(#[$attr:meta])* $vis:vis $name:ident, $t:ty) => {
46        $(#[$attr])*
47        $vis static $name: $crate::task::LocalKey<$t> = {
48            ::std::thread_local! {
49                static __KEY: ::core::cell::RefCell<::core::option::Option<$t>> =
50                    const { ::core::cell::RefCell::new(::core::option::Option::None) };
51            }
52            $crate::task::LocalKey { inner: __KEY }
53        };
54    };
55}
56
57/// A task-local key, declared with [`task_local!`](crate::task_local).
58pub struct LocalKey<T: 'static> {
59    #[doc(hidden)]
60    pub inner: std::thread::LocalKey<RefCell<Option<T>>>,
61}
62
63/// The key was read where no scope set it.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub struct AccessError {
66    _private: (),
67}
68
69impl fmt::Display for AccessError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.write_str("task-local value not set")
72    }
73}
74
75impl std::error::Error for AccessError {}
76
77impl<T: 'static> LocalKey<T> {
78    /// A future that runs `future` with the key set to `value` in each of its
79    /// polls, and drops `future` (when it is dropped early) with the value
80    /// still set.
81    pub fn scope<F: Future>(&'static self, value: T, future: F) -> TaskLocalFuture<T, F> {
82        TaskLocalFuture {
83            key: self,
84            slot: Some(value),
85            future: Some(future),
86            _pinned: PhantomPinned,
87        }
88    }
89
90    /// Runs `f` with the key set to `value`, then unsets it.
91    pub fn sync_scope<F: FnOnce() -> R, R>(&'static self, value: T, f: F) -> R {
92        let mut slot = Some(value);
93        self.enter(&mut slot, f)
94    }
95
96    /// Runs `f` on the key's value.
97    ///
98    /// # Panics
99    ///
100    /// Where no scope set the key; [`try_with`](Self::try_with) answers an
101    /// [`AccessError`] instead.
102    #[expect(
103        clippy::panic,
104        reason = "documented under `# Panics`; `try_with` answers the error instead"
105    )]
106    pub fn with<F: FnOnce(&T) -> R, R>(&'static self, f: F) -> R {
107        match self.try_with(f) {
108            Ok(r) => r,
109            Err(e) => panic!("{e}: no `scope` set this task-local key"),
110        }
111    }
112
113    /// Runs `f` on the key's value, or answers an [`AccessError`] where no
114    /// scope set it.
115    pub fn try_with<F: FnOnce(&T) -> R, R>(&'static self, f: F) -> Result<R, AccessError> {
116        self.inner
117            .try_with(|cell| cell.borrow().as_ref().map(f))
118            .ok()
119            .flatten()
120            .ok_or(AccessError { _private: () })
121    }
122
123    /// A clone of the key's value.
124    ///
125    /// # Panics
126    ///
127    /// Where no scope set the key.
128    pub fn get(&'static self) -> T
129    where
130        T: Clone,
131    {
132        self.with(T::clone)
133    }
134
135    /// Moves `slot`'s value into the key for `f`, and back out after it, even
136    /// when `f` panics: an outer scope's value is put back too.
137    #[expect(
138        clippy::panic,
139        reason = "a key used during this thread's teardown, where no value can be swapped in"
140    )]
141    fn enter<F: FnOnce() -> R, R>(&'static self, slot: &mut Option<T>, f: F) -> R {
142        struct Restore<'a, T: 'static> {
143            key: &'static LocalKey<T>,
144            slot: &'a mut Option<T>,
145        }
146        impl<T: 'static> Drop for Restore<'_, T> {
147            fn drop(&mut self) {
148                let _ = self.key.inner.try_with(|cell| {
149                    core::mem::swap(&mut *cell.borrow_mut(), self.slot);
150                });
151            }
152        }
153        let swapped = self
154            .inner
155            .try_with(|cell| core::mem::swap(&mut *cell.borrow_mut(), slot));
156        if swapped.is_err() {
157            panic!("the task-local key is used while this thread's locals are torn down");
158        }
159        let _restore = Restore { key: self, slot };
160        f()
161    }
162}
163
164impl<T: 'static> fmt::Debug for LocalKey<T> {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        f.debug_struct("LocalKey").finish_non_exhaustive()
167    }
168}
169
170/// The future of [`LocalKey::scope`].
171pub struct TaskLocalFuture<T: 'static, F> {
172    key: &'static LocalKey<T>,
173    /// The value while the future is not being polled.
174    slot: Option<T>,
175    /// The scoped future, until it completes or is dropped.
176    future: Option<F>,
177    _pinned: PhantomPinned,
178}
179
180impl<T: 'static, F: Future> Future for TaskLocalFuture<T, F> {
181    type Output = F::Output;
182
183    #[expect(
184        clippy::panic,
185        reason = "polled after it completed, which the `Future` contract rules out"
186    )]
187    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
188        // SAFETY: `future` is never moved out while pinned (it is dropped in
189        // place, by `Option::take` only after it completed or in `Drop`), and
190        // `key`, `slot` are not structurally pinned.
191        let this = unsafe { self.get_unchecked_mut() };
192        let key = this.key;
193        let slot = &mut this.slot;
194        let future = &mut this.future;
195        key.enter(slot, || {
196            let Some(inner) = future.as_mut() else {
197                panic!("`TaskLocalFuture` polled after it completed");
198            };
199            // SAFETY: see above; the inner future stays where it is.
200            let poll = unsafe { Pin::new_unchecked(inner) }.poll(cx);
201            if poll.is_ready() {
202                *future = None;
203            }
204            poll
205        })
206    }
207}
208
209impl<T: 'static, F> Drop for TaskLocalFuture<T, F> {
210    fn drop(&mut self) {
211        if self.future.is_some() {
212            let future = &mut self.future;
213            // The scoped future's drop sees the value, as its polls did; a key
214            // that cannot be entered any more (locals torn down) drops it
215            // without.
216            let key = self.key;
217            let entered = key.inner.try_with(|_| ()).is_ok();
218            if entered {
219                key.enter(&mut self.slot, || *future = None);
220            } else {
221                *future = None;
222            }
223        }
224    }
225}
226
227impl<T: 'static + fmt::Debug, F> fmt::Debug for TaskLocalFuture<T, F> {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.debug_struct("TaskLocalFuture")
230            .field("value", &self.slot)
231            .finish_non_exhaustive()
232    }
233}