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 {}