Skip to main content

dial9_core/
primitives.rs

1//! Cfg-gated concurrency primitives.
2//!
3//! Under normal compilation this re-exports from `std`. With `--cfg shuttle`
4//! it re-exports from [`shuttle`], giving the shuttle scheduler control over all
5//! synchronization points so that tests can explore thread interleavings
6//! deterministically.
7
8// ── std path (production) ───────────────────────────────────────────────────
9
10#[cfg(not(shuttle))]
11pub mod sync {
12    pub use std::sync::atomic;
13    pub use std::sync::mpsc;
14    #[allow(unused_imports)]
15    pub use std::sync::{Arc, Barrier, Mutex, Weak};
16}
17
18#[cfg(not(shuttle))]
19pub mod thread {
20    #[allow(unused_imports)]
21    pub use std::thread::{JoinHandle, sleep, spawn};
22
23    /// Spawn a named thread. Uses `std::thread::Builder` in production,
24    /// falls back to plain `spawn` under shuttle (which has no Builder).
25    pub fn spawn_named<F, T>(name: &str, f: F) -> JoinHandle<T>
26    where
27        F: FnOnce() -> T + Send + 'static,
28        T: Send + 'static,
29    {
30        std::thread::Builder::new()
31            .name(name.into())
32            .spawn(f)
33            .expect("failed to spawn thread")
34    }
35}
36
37#[cfg(not(shuttle))]
38#[macro_export]
39macro_rules! define_thread_local {
40    ($($tt:tt)*) => { std::thread_local! { $($tt)* } };
41}
42#[cfg(not(shuttle))]
43pub use crate::define_thread_local as thread_local;
44
45// ── shuttle path (deterministic testing) ────────────────────────────────────
46
47#[cfg(shuttle)]
48pub mod sync {
49    pub use shuttle::sync::atomic;
50    #[allow(unused_imports)]
51    pub use shuttle::sync::{Arc, Barrier, Mutex, Weak};
52
53    /// Wrapper around shuttle's mpsc that adds random timeouts to
54    /// `recv_timeout`. Shuttle's built-in `recv_timeout` ignores the
55    /// timeout and blocks unconditionally, which means the flush loop
56    /// never loops. This wrapper randomly returns `Timeout` so shuttle
57    /// can explore interleavings where the flush loop actually runs
58    /// multiple cycles.
59    pub mod mpsc {
60        pub use shuttle::sync::mpsc::{RecvTimeoutError, SyncSender};
61
62        pub struct Receiver<T> {
63            inner: shuttle::sync::mpsc::Receiver<T>,
64        }
65
66        // shuttle::sync::mpsc::Receiver is Send but the wrapper needs to be too
67        // SAFETY: shuttle's Receiver<T> is Send when T: Send
68        unsafe impl<T: Send> Send for Receiver<T> {}
69
70        impl<T> Receiver<T> {
71            pub fn recv_timeout(
72                &self,
73                _timeout: std::time::Duration,
74            ) -> Result<T, RecvTimeoutError> {
75                // Randomly decide whether to simulate a timeout, giving
76                // the flush loop a chance to execute its body.
77                if shuttle::rand::thread_rng().gen_bool(0.8) {
78                    match self.inner.try_recv() {
79                        Ok(val) => Ok(val),
80                        Err(shuttle::sync::mpsc::TryRecvError::Empty) => {
81                            Err(RecvTimeoutError::Timeout)
82                        }
83                        Err(shuttle::sync::mpsc::TryRecvError::Disconnected) => {
84                            Err(RecvTimeoutError::Disconnected)
85                        }
86                    }
87                } else {
88                    // Delegate to shuttle's blocking recv to explore the
89                    // "flush loop blocks waiting for command" path.
90                    self.inner
91                        .recv()
92                        .map_err(|_| RecvTimeoutError::Disconnected)
93                }
94            }
95
96            pub fn recv(&self) -> Result<T, shuttle::sync::mpsc::RecvError> {
97                self.inner.recv()
98            }
99        }
100
101        use shuttle::rand::Rng;
102
103        /// Wraps shuttle's `sync_channel` to return our `Receiver` wrapper.
104        pub fn sync_channel<T>(bound: usize) -> (SyncSender<T>, Receiver<T>) {
105            let (tx, rx) = shuttle::sync::mpsc::sync_channel(bound);
106            (tx, Receiver { inner: rx })
107        }
108    }
109}
110
111#[cfg(shuttle)]
112pub mod thread {
113    #[allow(unused_imports)]
114    pub use shuttle::thread::{JoinHandle, sleep, spawn};
115
116    pub fn spawn_named<F, T>(_name: &str, f: F) -> JoinHandle<T>
117    where
118        F: FnOnce() -> T + Send + 'static,
119        T: Send + 'static,
120    {
121        spawn(f)
122    }
123}
124
125#[cfg(shuttle)]
126#[macro_export]
127macro_rules! define_thread_local {
128    ($($tt:tt)*) => { shuttle::thread_local! { $($tt)* } };
129}
130#[cfg(shuttle)]
131pub use crate::define_thread_local as thread_local;
132
133#[cfg(not(shuttle))]
134pub mod fs {
135    use std::io::{self, Write};
136    use std::path::Path;
137
138    pub fn create_dir_all(path: &Path) -> io::Result<()> {
139        std::fs::create_dir_all(path)
140    }
141    pub fn rename(from: &Path, to: &Path) -> io::Result<()> {
142        std::fs::rename(from, to)
143    }
144    pub fn remove_file(path: &Path) -> io::Result<()> {
145        std::fs::remove_file(path)
146    }
147    pub fn remove_dir(path: &Path) -> io::Result<()> {
148        std::fs::remove_dir(path)
149    }
150    pub fn read_dir(path: &Path) -> io::Result<std::fs::ReadDir> {
151        std::fs::read_dir(path)
152    }
153    pub fn metadata(path: &Path) -> io::Result<std::fs::Metadata> {
154        std::fs::metadata(path)
155    }
156    pub fn read(path: &Path) -> io::Result<Vec<u8>> {
157        std::fs::read(path)
158    }
159
160    /// Active-segment file handle.
161    #[derive(Debug)]
162    pub struct File(std::fs::File);
163
164    impl File {
165        pub fn create(path: &Path) -> io::Result<File> {
166            std::fs::File::create(path).map(File)
167        }
168    }
169
170    impl Write for File {
171        #[inline]
172        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
173            self.0.write(buf)
174        }
175        #[inline]
176        fn flush(&mut self) -> io::Result<()> {
177            self.0.flush()
178        }
179    }
180}
181
182#[cfg(shuttle)]
183pub mod fs {
184    use std::cell::Cell;
185    use std::io::{self, ErrorKind, Write};
186    use std::path::Path;
187
188    use shuttle::rand::Rng;
189
190    /// How to fail filesystem operations during a shuttle run.
191    #[derive(Clone, Copy, Debug)]
192    pub enum FaultPolicy {
193        /// Delegate to real `std::fs`, nothing fails.
194        None,
195        /// Every fallible op returns `PermissionDenied`.
196        FailAll,
197        /// Each op independently fails with this probability, drawn from
198        /// shuttle's RNG so the scheduler explores the fault schedule.
199        FailProb(f64),
200    }
201
202    std::thread_local! {
203        static FAULT: Cell<FaultPolicy> = const { Cell::new(FaultPolicy::None) };
204    }
205
206    /// Arm `policy`: the returned guard restores the previous one on drop so a
207    /// fault can't leak into the next shuttle iteration.
208    #[must_use]
209    pub fn set_fault(policy: FaultPolicy) -> FaultGuard {
210        let prev = FAULT.with(|f| f.replace(policy));
211        FaultGuard { prev }
212    }
213
214    pub struct FaultGuard {
215        prev: FaultPolicy,
216    }
217
218    impl Drop for FaultGuard {
219        fn drop(&mut self) {
220            FAULT.with(|f| f.set(self.prev));
221        }
222    }
223
224    fn check() -> io::Result<()> {
225        let fail = match FAULT.with(|f| f.get()) {
226            FaultPolicy::None => false,
227            FaultPolicy::FailAll => true,
228            FaultPolicy::FailProb(p) => shuttle::rand::thread_rng().gen_bool(p),
229        };
230        if fail {
231            Err(io::Error::from(ErrorKind::PermissionDenied))
232        } else {
233            Ok(())
234        }
235    }
236
237    pub fn create_dir_all(path: &Path) -> io::Result<()> {
238        check()?;
239        std::fs::create_dir_all(path)
240    }
241    pub fn rename(from: &Path, to: &Path) -> io::Result<()> {
242        check()?;
243        std::fs::rename(from, to)
244    }
245    pub fn remove_file(path: &Path) -> io::Result<()> {
246        check()?;
247        std::fs::remove_file(path)
248    }
249    pub fn remove_dir(path: &Path) -> io::Result<()> {
250        check()?;
251        std::fs::remove_dir(path)
252    }
253    pub fn read_dir(path: &Path) -> io::Result<std::fs::ReadDir> {
254        check()?;
255        std::fs::read_dir(path)
256    }
257    pub fn metadata(path: &Path) -> io::Result<std::fs::Metadata> {
258        check()?;
259        std::fs::metadata(path)
260    }
261    pub fn read(path: &Path) -> io::Result<Vec<u8>> {
262        check()?;
263        std::fs::read(path)
264    }
265
266    /// Active-segment file handle whose `write`/`flush` honor the armed fault
267    /// policy. `create` is never faulted, so a writer can always be built.
268    #[derive(Debug)]
269    pub struct File(std::fs::File);
270
271    impl File {
272        pub fn create(path: &Path) -> io::Result<File> {
273            std::fs::File::create(path).map(File)
274        }
275    }
276
277    impl Write for File {
278        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
279            check()?;
280            self.0.write(buf)
281        }
282        fn flush(&mut self) -> io::Result<()> {
283            check()?;
284            self.0.flush()
285        }
286    }
287}
288
289// ── BoundedQueue ────────────────────────────────────────────────────────────
290
291/// A bounded MPMC queue. Production uses `crossbeam_queue::ArrayQueue`;
292/// under shuttle it uses a `Mutex<VecDeque>` so the scheduler can control
293/// access.
294#[cfg(not(shuttle))]
295pub struct BoundedQueue<T> {
296    inner: crossbeam_queue::ArrayQueue<T>,
297}
298
299#[cfg(not(shuttle))]
300impl<T> BoundedQueue<T> {
301    pub fn new(capacity: usize) -> Self {
302        Self {
303            inner: crossbeam_queue::ArrayQueue::new(capacity),
304        }
305    }
306
307    /// Push a value, evicting the oldest if full. Returns the evicted value.
308    pub fn force_push(&self, value: T) -> Option<T> {
309        self.inner.force_push(value)
310    }
311
312    pub fn pop(&self) -> Option<T> {
313        self.inner.pop()
314    }
315}
316
317#[cfg(shuttle)]
318pub struct BoundedQueue<T> {
319    inner: shuttle::sync::Mutex<std::collections::VecDeque<T>>,
320    capacity: usize,
321}
322
323#[cfg(shuttle)]
324impl<T> BoundedQueue<T> {
325    pub fn new(capacity: usize) -> Self {
326        Self {
327            inner: shuttle::sync::Mutex::new(std::collections::VecDeque::with_capacity(capacity)),
328            capacity,
329        }
330    }
331
332    pub fn force_push(&self, value: T) -> Option<T> {
333        let mut q = self.inner.lock().unwrap();
334        let evicted = if q.len() >= self.capacity {
335            q.pop_front()
336        } else {
337            None
338        };
339        q.push_back(value);
340        evicted
341    }
342
343    pub fn pop(&self) -> Option<T> {
344        self.inner.lock().unwrap().pop_front()
345    }
346}