Skip to main content

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}