Skip to main content

nuclei/
shard_local.rs

1//! Shard-local values: [`shard_local!`](crate::shard_local) declares a
2//! [`ShardLocal`], which holds one value per worker shard, made on its first
3//! use there. The value lives in the shard, not in the thread: when a
4//! worker's shard is handed to another thread (its runner blocked in one
5//! poll, see [`Builder::handoff_after`](crate::Builder::handoff_after)), the
6//! value goes with it, and the shard's tasks keep seeing it there.
7
8use core::fmt;
9
10use crate::rt::context;
11use crate::rt::shard::locals::Missing;
12use core::sync::atomic::{AtomicUsize, Ordering};
13
14/// Declares shard-local statics, each a [`ShardLocal`] made with the
15/// expression given on its first use on each worker.
16///
17/// ```
18/// use std::cell::Cell;
19///
20/// nuclei::shard_local! {
21///     static REQUESTS: Cell<u64> = Cell::new(0);
22/// }
23///
24/// let rt = nuclei::Runtime::builder().worker_threads(1).build().unwrap();
25/// let count = rt.block_on(rt.spawn(async {
26///     REQUESTS.with(|n| n.set(n.get() + 1));
27///     REQUESTS.with(Cell::get)
28/// }));
29/// assert_eq!(count, 1);
30/// ```
31#[macro_export]
32macro_rules! shard_local {
33    () => {};
34    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr; $($rest:tt)*) => {
35        $(#[$attr])*
36        $vis static $name: $crate::runtime::ShardLocal<$t> =
37            $crate::runtime::ShardLocal::__new(|| $init);
38        $crate::shard_local!($($rest)*);
39    };
40    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr) => {
41        $(#[$attr])*
42        $vis static $name: $crate::runtime::ShardLocal<$t> =
43            $crate::runtime::ShardLocal::__new(|| $init);
44    };
45}
46
47/// A value per worker shard, declared with
48/// [`shard_local!`](crate::shard_local).
49///
50/// Each worker of each runtime has its own value, made by the declaration's
51/// expression the first time code running on that worker uses it, and
52/// dropped with the worker's shard. Only code running on a worker (its
53/// tasks, and `block_on` called from them) reaches a value: a thread's own
54/// shard in `block_on`, the blocking pool and any other thread have none.
55///
56/// A task spawned on a worker stays on that worker's shard, and a handoff
57/// moves the shard with its values, so the task always sees one value. A
58/// task kept on a thread by `spawn_local` whose worker's shard was handed
59/// on runs on that thread's own shard, off the worker: it gets
60/// [`ShardAccessError`] there.
61pub struct ShardLocal<T: Send + 'static> {
62    init: fn() -> T,
63    /// The static's place among every shard's values, plus one; 0 until its
64    /// first use.
65    key: AtomicUsize,
66}
67
68/// Keys handed out so far.
69static KEYS: AtomicUsize = AtomicUsize::new(0);
70
71impl<T: Send + 'static> ShardLocal<T> {
72    #[doc(hidden)]
73    pub const fn __new(init: fn() -> T) -> Self {
74        Self {
75            init,
76            key: AtomicUsize::new(0),
77        }
78    }
79
80    /// The static's key, taken on its first use.
81    fn key(&self) -> usize {
82        let key = self.key.load(Ordering::Acquire);
83        if key != 0 {
84            return key - 1;
85        }
86        let mine = KEYS.fetch_add(1, Ordering::Relaxed) + 1;
87        match self
88            .key
89            .compare_exchange(0, mine, Ordering::AcqRel, Ordering::Acquire)
90        {
91            Ok(_) => mine - 1,
92            Err(won) => won - 1,
93        }
94    }
95
96    /// Runs `f` on this worker's value, made now if this is its first use
97    /// here.
98    ///
99    /// While `f` runs the shard stays on this thread, even across a
100    /// `block_on` inside `f`: keep `f` short, as a blocking `f` holds the
101    /// whole worker up.
102    ///
103    /// # Panics
104    ///
105    /// Off a worker, or when the value's own initializer uses it;
106    /// [`try_with`](Self::try_with) answers the error instead.
107    #[expect(
108        clippy::panic,
109        reason = "documented under `# Panics`; `try_with` answers the error instead"
110    )]
111    pub fn with<R>(&'static self, f: impl FnOnce(&T) -> R) -> R {
112        match self.try_with(f) {
113            Ok(r) => r,
114            Err(err) => panic!("{err}"),
115        }
116    }
117
118    /// Runs `f` on this worker's value, as [`with`](Self::with) does, or
119    /// answers a [`ShardAccessError`] off a worker or while the value's own
120    /// initializer runs.
121    pub fn try_with<R>(&'static self, f: impl FnOnce(&T) -> R) -> Result<R, ShardAccessError> {
122        let key = self.key();
123        context::current_pinned(|shard| {
124            let locals = shard
125                .and_then(|shard| shard.locals())
126                .ok_or(ShardAccessError { making: false })?;
127            match locals.get_or_init(key, self.init) {
128                Ok(value) => Ok(f(value)),
129                Err(Missing::Making) => Err(ShardAccessError { making: true }),
130            }
131        })
132    }
133}
134
135impl<T: Send + 'static> fmt::Debug for ShardLocal<T> {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("ShardLocal").finish_non_exhaustive()
138    }
139}
140
141/// A [`ShardLocal`] was used where it has no value: off a worker, or by its
142/// own initializer.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct ShardAccessError {
145    making: bool,
146}
147
148impl ShardAccessError {
149    /// Whether the value was being made: its initializer used it.
150    pub fn is_initializing(&self) -> bool {
151        self.making
152    }
153}
154
155impl fmt::Display for ShardAccessError {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        f.write_str(if self.making {
158            "the shard-local value was used by its own initializer"
159        } else {
160            "this code runs on no worker, so it has no shard-local value: use it in a task"
161        })
162    }
163}
164
165impl std::error::Error for ShardAccessError {}