shuttle_std/sync/once.rs
1use crate::sync::{Mutex, ResourceSignature, ResourceType};
2use shuttle_engine::runtime::execution::ExecutionState;
3use shuttle_engine::runtime::storage::StorageKey;
4use shuttle_engine::runtime::task::clock::VectorClock;
5use std::cell::RefCell;
6use std::rc::Rc;
7use std::sync::atomic::{AtomicUsize as StdAtomicUsize, Ordering};
8use std::sync::LazyLock;
9use tracing::trace;
10
11/// A synchronization primitive which can be used to run a one-time global initialization. Useful
12/// for one-time initialization for FFI or related functionality. This type can only be constructed
13/// with [`Once::new()`].
14#[derive(Debug)]
15pub struct Once {
16 // Note that we need to use a `LazyLock` here (or similar) so that tests don't interfere with each other when multiple
17 // tests try to interact with a `Once` at the same time. This happens in our `Lazy` implementation, and also happens
18 // whenever there is a `static Once`, such as in the following:
19 // ```
20 // static O: Once = Once::new();
21 //
22 // #[test]
23 // fn foo1() {
24 // check_dfs(|| O.call_once(|| {}), None);
25 // }
26 //
27 // #[test]
28 // fn foo2() {
29 // check_dfs(|| O.call_once(|| {}), None);
30 // }
31 // ```
32 /// Unique identifier for this [`Once`], used as a key for the [`OnceInitState`] for this instance.
33 id: LazyLock<usize>,
34 signature: ResourceSignature,
35}
36
37/// A `Once` cell can either be `Running`, in which case a `Mutex` mediates racing threads trying to
38/// invoke `call_once`, or `Complete` once an initializer has completed, in which case the `Mutex`
39/// is no longer necessary.
40enum OnceInitState {
41 Running(Rc<Mutex<bool>>),
42 Complete(VectorClock),
43}
44
45impl std::fmt::Debug for OnceInitState {
46 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
47 match self {
48 Self::Running(_) => write!(f, "Running"),
49 Self::Complete(_) => write!(f, "Complete"),
50 }
51 }
52}
53
54impl Once {
55 /// Creates a new `Once` value.
56 #[must_use]
57 #[allow(clippy::new_without_default)]
58 #[track_caller]
59 pub const fn new() -> Self {
60 static NEXT_ID: StdAtomicUsize = StdAtomicUsize::new(1);
61
62 Self {
63 id: LazyLock::new(|| {
64 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
65 assert_ne!(id, 0, "id overflow");
66 id
67 }),
68 signature: ResourceSignature::new_const(ResourceType::Once),
69 }
70 }
71
72 /// Performs an initialization routine once and only once. The given closure will be executed
73 /// if this is the first time `call_once` has been called, and otherwise the routine will *not*
74 /// be invoked.
75 ///
76 /// This method will block the calling thread if another initialization routine is currently
77 /// running.
78 ///
79 /// When this function returns, it is guaranteed that some initialization has run and completed
80 /// (it may not be the closure specified).
81 pub fn call_once<F>(&self, f: F)
82 where
83 F: FnOnce(),
84 {
85 self.call_once_inner(|_state| f(), false);
86 }
87
88 /// Performs the same function as [`Once::call_once()`] except ignores poisoning.
89 ///
90 /// If the cell has previously been poisoned, this function will still attempt to call the given
91 /// closure. If the closure does not panic, the cell will no longer be poisoned.
92 pub fn call_once_force<F>(&self, f: F)
93 where
94 F: FnOnce(&OnceState),
95 {
96 self.call_once_inner(f, true);
97 }
98
99 /// Returns `true` if some [`Once::call_once()`] call has completed successfully.
100 pub fn is_completed(&self) -> bool {
101 ExecutionState::with(|state| {
102 let init = match self.get_state(state) {
103 Some(init) => init,
104 None => return false,
105 };
106 let init_state = init.borrow();
107 match &*init_state {
108 OnceInitState::Complete(clock) => {
109 let clock = clock.clone();
110 drop(init_state);
111 state.update_clock(&clock);
112 true
113 }
114 _ => false,
115 }
116 })
117 }
118
119 fn call_once_inner<F>(&self, f: F, ignore_poisoning: bool)
120 where
121 F: FnOnce(&OnceState),
122 {
123 let lock = ExecutionState::with(|state| {
124 // Initialize the state of the `Once` cell if we're the first thread to try
125 if self.get_state(state).is_none() {
126 self.init_state(
127 state,
128 OnceInitState::Running(Rc::new(Mutex::new_internal(false, self.signature.clone()))),
129 );
130 }
131
132 let init = self.get_state(state).expect("must be initialized by this point");
133 let init_state = init.borrow();
134 trace!(state=?init_state, "call_once on cell {:p}", self);
135 match &*init_state {
136 OnceInitState::Complete(clock) => {
137 // If already complete, just update the clock from the thread that inited
138 let clock = clock.clone();
139 drop(init_state);
140 state.update_clock(&clock);
141 None
142 }
143 OnceInitState::Running(lock) => Some(Rc::clone(lock)),
144 }
145 });
146
147 // If there's a lock, then we need to try racing on it to decide who gets to run their
148 // initialization closure.
149 if let Some(lock) = lock {
150 let (mut flag, is_poisoned) = match lock.lock() {
151 Ok(flag) => (flag, false),
152 Err(_) if !ignore_poisoning => panic!("Once instance has previously been poisoned"),
153 Err(err) => (err.into_inner(), true),
154 };
155 if *flag {
156 return;
157 }
158
159 trace!("won the call_once race for cell {:p}", self);
160 f(&OnceState(is_poisoned));
161
162 *flag = true;
163 // We were the thread that won the race, so remember our current clock to establish
164 // causality with future threads that try (and fail) to run `call_once`. The threads
165 // that were racing with us will get causality through acquiring the `Mutex`.
166 ExecutionState::with(|state| {
167 let clock = state.increment_clock().clone();
168 *self
169 .get_state(state)
170 .expect("must be initialized by this point")
171 .borrow_mut() = OnceInitState::Complete(clock);
172 });
173 }
174 }
175
176 fn id(&self) -> usize {
177 *self.id
178 }
179
180 fn get_state<'a>(&self, from: &'a ExecutionState) -> Option<&'a RefCell<OnceInitState>> {
181 from.get_storage::<_, RefCell<OnceInitState>>(self)
182 }
183
184 fn init_state(&self, into: &mut ExecutionState, new_state: OnceInitState) {
185 into.init_storage::<_, RefCell<OnceInitState>>(self, RefCell::new(new_state));
186 }
187}
188
189/// State yielded to [`Once::call_once_force()`]'s closure parameter. The state can be used to query
190/// the poison status of the [`Once`].
191#[derive(Debug)]
192#[non_exhaustive]
193pub struct OnceState(bool);
194
195impl OnceState {
196 /// Returns `true` if the associated [`Once`] was poisoned prior to the invocation of the
197 /// closure passed to [`Once::call_once_force()`].
198 pub fn is_poisoned(&self) -> bool {
199 self.0
200 }
201}
202
203impl From<&Once> for StorageKey {
204 fn from(once: &Once) -> Self {
205 StorageKey(once.id(), 0x2)
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn unique_resource_signature_once() {
215 shuttle_schedulers::check_random(
216 || {
217 let once1 = Once::new();
218 let once2 = Once::new();
219 assert_ne!(once1.signature, once2.signature);
220 },
221 1,
222 );
223 }
224}