Skip to main content

subetha_cxc/
blocking_rw_lock.rs

1//! `BlockingRWLock`: cross-process reader-writer lock with a
2//! kernel-park slow path via [`CrossProcessWaker`].
3//!
4//! Composes [`crate::shared_rw_lock::SharedRWLock`]
5//! with one `CrossProcessWaker` plus a small mmap-backed
6//! wakeup-generation counter. The hot path delegates to
7//! `try_read_lock` / `try_write_lock` (single CAS each). The
8//! contention slow path differs from the existing primitive:
9//!
10//! - **`SharedRWLock::read_lock` / `write_lock`** spin → yield →
11//!   `sleep(50us)` indefinitely until the lock becomes available.
12//!   The sleep tail burns CPU on each wake-up tick AND can miss
13//!   an unlock by up to 50us.
14//! - **`BlockingRWLock::read_park` / `write_park`** register in
15//!   the waker at the current generation, then park in the kernel.
16//!   The kernel returns within microseconds of the next unlock.
17//!
18//! Both readers and writers park on the SAME waker; an unlock
19//! fires `wake_all` so the contending side picks up the lock
20//! according to the underlying writer-priority policy.
21
22use std::fs::OpenOptions;
23use std::path::{Path, PathBuf};
24use std::sync::Arc;
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::time::{Duration, Instant};
27
28use memmap2::{MmapMut, MmapOptions};
29
30use crate::cross_process_waker::{
31    CrossProcessWaker, MAX_WAITERS_DEFAULT, WakerError,
32};
33use crate::shared_rw_lock::{RWLockError, SharedRWLock};
34
35const WAKEUP_MAGIC: u64 = 0x4257_524C_5742_4B30; // "BWRLWBK0"
36const WAKEUP_REGION_SIZE: usize = 64;
37const WAKEUP_OFFSET: usize = 8;
38
39/// Errors returned by [`BlockingRWLock`] operations.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum BlockingRWLockError {
42    Lock(RWLockError),
43    Waker(WakerError),
44    Timeout,
45    LayoutMismatch,
46    Io(std::io::ErrorKind),
47}
48
49impl From<RWLockError> for BlockingRWLockError {
50    fn from(e: RWLockError) -> Self { Self::Lock(e) }
51}
52impl From<WakerError> for BlockingRWLockError {
53    fn from(e: WakerError) -> Self {
54        match e {
55            WakerError::Timeout => Self::Timeout,
56            other => Self::Waker(other),
57        }
58    }
59}
60impl From<std::io::Error> for BlockingRWLockError {
61    fn from(e: std::io::Error) -> Self { Self::Io(e.kind()) }
62}
63
64#[allow(dead_code)]
65enum WakeupBacking {
66    Anon(MmapMut),
67    File(std::fs::File, MmapMut),
68}
69
70struct WakeupAtom {
71    #[allow(dead_code)]
72    backing: WakeupBacking,
73    ptr: *const AtomicU64,
74}
75
76// SAFETY: AtomicU64 ptr points into the mmap held by `backing`,
77// valid for the lifetime of WakeupAtom.
78unsafe impl Send for WakeupAtom {}
79unsafe impl Sync for WakeupAtom {}
80
81impl WakeupAtom {
82    /// Obtain the wakeup region at `path`, initializing it only when the path
83    /// does not yet exist. Attaching leaves a live generation counter in
84    /// place.
85    fn create_file(path: &Path) -> Result<Self, BlockingRWLockError> {
86        let (file, mut mmap) = crate::mmf_attach::create_or_attach(
87            path,
88            WAKEUP_REGION_SIZE,
89            |base| unsafe {
90                (base.add(WAKEUP_OFFSET) as *mut AtomicU64).write(AtomicU64::new(0));
91                std::ptr::write_volatile(base as *mut u64, WAKEUP_MAGIC);
92            },
93            |base| unsafe { (base as *const u64).read() == WAKEUP_MAGIC },
94        )?;
95        let base = mmap.as_mut_ptr();
96        let ptr = unsafe { base.add(WAKEUP_OFFSET) as *const AtomicU64 };
97        Ok(Self { backing: WakeupBacking::File(file, mmap), ptr })
98    }
99
100    fn open_file(path: &Path) -> Result<Self, BlockingRWLockError> {
101        let file = OpenOptions::new().read(true).write(true).open(path)?;
102        if (file.metadata()?.len() as usize) < WAKEUP_REGION_SIZE {
103            return Err(BlockingRWLockError::LayoutMismatch);
104        }
105        let mut mmap = unsafe { MmapOptions::new().len(WAKEUP_REGION_SIZE).map_mut(&file)? };
106        let base = mmap.as_mut_ptr();
107        let magic = unsafe { (base as *const u64).read() };
108        if magic != WAKEUP_MAGIC {
109            return Err(BlockingRWLockError::LayoutMismatch);
110        }
111        let ptr = unsafe { base.add(WAKEUP_OFFSET) as *const AtomicU64 };
112        Ok(Self { backing: WakeupBacking::File(file, mmap), ptr })
113    }
114
115    #[inline]
116    fn atom(&self) -> &AtomicU64 { unsafe { &*self.ptr } }
117}
118
119/// Cross-process reader-writer lock with kernel-park slow path.
120pub struct BlockingRWLock {
121    inner: Arc<SharedRWLock>,
122    waker: Arc<CrossProcessWaker>,
123    wakeup: Arc<WakeupAtom>,
124}
125
126const PRE_PARK_SPIN: u32 = 32;
127
128impl BlockingRWLock {
129    /// Create a blocking rwlock. Lays out three files under one
130    /// base path: `<base>.rwlock.bin` for the state atom,
131    /// `<base>.waker.bin` for the waker slots, `<base>.wakeup.bin`
132    /// for the magic + generation atom.
133    pub fn create(base_path: impl AsRef<Path>) -> Result<Self, BlockingRWLockError> {
134        let base = base_path.as_ref();
135        let inner = SharedRWLock::create(rwlock_path(base))?;
136        let waker = CrossProcessWaker::create(waker_path(base), MAX_WAITERS_DEFAULT)?;
137        let wakeup = WakeupAtom::create_file(&wakeup_path(base))?;
138        Ok(Self {
139            inner: Arc::new(inner),
140            waker: Arc::new(waker),
141            wakeup: Arc::new(wakeup),
142        })
143    }
144
145    /// Open an existing blocking rwlock.
146    pub fn open(base_path: impl AsRef<Path>) -> Result<Self, BlockingRWLockError> {
147        let base = base_path.as_ref();
148        let inner = SharedRWLock::open(rwlock_path(base))?;
149        let waker = CrossProcessWaker::open(waker_path(base), MAX_WAITERS_DEFAULT)?;
150        let wakeup = WakeupAtom::open_file(&wakeup_path(base))?;
151        Ok(Self {
152            inner: Arc::new(inner),
153            waker: Arc::new(waker),
154            wakeup: Arc::new(wakeup),
155        })
156    }
157
158    /// Non-blocking read-lock attempt.
159    pub fn try_read_lock(&self) -> Result<BlockingReadGuard<'_>, BlockingRWLockError> {
160        match self.inner.try_read_lock() {
161            Ok(g) => {
162                std::mem::forget(g);
163                Ok(BlockingReadGuard { lock: self })
164            }
165            Err(e) => Err(BlockingRWLockError::Lock(e)),
166        }
167    }
168
169    /// Non-blocking write-lock attempt.
170    pub fn try_write_lock(&self) -> Result<BlockingWriteGuard<'_>, BlockingRWLockError> {
171        match self.inner.try_write_lock() {
172            Ok(g) => {
173                std::mem::forget(g);
174                Ok(BlockingWriteGuard { lock: self })
175            }
176            Err(e) => Err(BlockingRWLockError::Lock(e)),
177        }
178    }
179
180    /// Read-lock with kernel-park slow path.
181    pub fn read_park(&self) -> Result<BlockingReadGuard<'_>, BlockingRWLockError> {
182        loop {
183            if let Ok(g) = self.inner.try_read_lock() {
184                std::mem::forget(g);
185                return Ok(BlockingReadGuard { lock: self });
186            }
187            for _ in 0..PRE_PARK_SPIN {
188                if let Ok(g) = self.inner.try_read_lock() {
189                    std::mem::forget(g);
190                    return Ok(BlockingReadGuard { lock: self });
191                }
192                std::hint::spin_loop();
193            }
194            let snapshot = self.wakeup.atom().load(Ordering::Acquire);
195            let token = self.waker.try_park(snapshot + 1)?;
196            if let Ok(g) = self.inner.try_read_lock() {
197                self.waker.release(token);
198                std::mem::forget(g);
199                return Ok(BlockingReadGuard { lock: self });
200            }
201            self.waker.wait(token, None)?;
202        }
203    }
204
205    /// Read-lock with bounded wait.
206    pub fn read_park_timeout(
207        &self,
208        timeout: Duration,
209    ) -> Result<BlockingReadGuard<'_>, BlockingRWLockError> {
210        let deadline = Instant::now() + timeout;
211        loop {
212            if let Ok(g) = self.inner.try_read_lock() {
213                std::mem::forget(g);
214                return Ok(BlockingReadGuard { lock: self });
215            }
216            for _ in 0..PRE_PARK_SPIN {
217                if let Ok(g) = self.inner.try_read_lock() {
218                    std::mem::forget(g);
219                    return Ok(BlockingReadGuard { lock: self });
220                }
221                std::hint::spin_loop();
222            }
223            let snapshot = self.wakeup.atom().load(Ordering::Acquire);
224            let token = self.waker.try_park(snapshot + 1)?;
225            if let Ok(g) = self.inner.try_read_lock() {
226                self.waker.release(token);
227                std::mem::forget(g);
228                return Ok(BlockingReadGuard { lock: self });
229            }
230            let now = Instant::now();
231            if now >= deadline {
232                self.waker.release(token);
233                return Err(BlockingRWLockError::Timeout);
234            }
235            let remaining = deadline - now;
236            match self.waker.wait(token, Some(remaining)) {
237                Ok(()) => continue,
238                Err(WakerError::Timeout) => return Err(BlockingRWLockError::Timeout),
239                Err(e) => return Err(BlockingRWLockError::Waker(e)),
240            }
241        }
242    }
243
244    /// Write-lock with kernel-park slow path.
245    pub fn write_park(&self) -> Result<BlockingWriteGuard<'_>, BlockingRWLockError> {
246        loop {
247            if let Ok(g) = self.inner.try_write_lock() {
248                std::mem::forget(g);
249                return Ok(BlockingWriteGuard { lock: self });
250            }
251            for _ in 0..PRE_PARK_SPIN {
252                if let Ok(g) = self.inner.try_write_lock() {
253                    std::mem::forget(g);
254                    return Ok(BlockingWriteGuard { lock: self });
255                }
256                std::hint::spin_loop();
257            }
258            let snapshot = self.wakeup.atom().load(Ordering::Acquire);
259            let token = self.waker.try_park(snapshot + 1)?;
260            if let Ok(g) = self.inner.try_write_lock() {
261                self.waker.release(token);
262                std::mem::forget(g);
263                return Ok(BlockingWriteGuard { lock: self });
264            }
265            self.waker.wait(token, None)?;
266        }
267    }
268
269    /// Write-lock with bounded wait.
270    pub fn write_park_timeout(
271        &self,
272        timeout: Duration,
273    ) -> Result<BlockingWriteGuard<'_>, BlockingRWLockError> {
274        let deadline = Instant::now() + timeout;
275        loop {
276            if let Ok(g) = self.inner.try_write_lock() {
277                std::mem::forget(g);
278                return Ok(BlockingWriteGuard { lock: self });
279            }
280            for _ in 0..PRE_PARK_SPIN {
281                if let Ok(g) = self.inner.try_write_lock() {
282                    std::mem::forget(g);
283                    return Ok(BlockingWriteGuard { lock: self });
284                }
285                std::hint::spin_loop();
286            }
287            let snapshot = self.wakeup.atom().load(Ordering::Acquire);
288            let token = self.waker.try_park(snapshot + 1)?;
289            if let Ok(g) = self.inner.try_write_lock() {
290                self.waker.release(token);
291                std::mem::forget(g);
292                return Ok(BlockingWriteGuard { lock: self });
293            }
294            let now = Instant::now();
295            if now >= deadline {
296                self.waker.release(token);
297                return Err(BlockingRWLockError::Timeout);
298            }
299            let remaining = deadline - now;
300            match self.waker.wait(token, Some(remaining)) {
301                Ok(()) => continue,
302                Err(WakerError::Timeout) => return Err(BlockingRWLockError::Timeout),
303                Err(e) => return Err(BlockingRWLockError::Waker(e)),
304            }
305        }
306    }
307
308    /// Bump wakeup-generation + wake every parker. Called from the
309    /// guard Drop paths after the inner lock is released.
310    fn signal_unlock(&self) {
311        let new_gen = self.wakeup.atom().fetch_add(1, Ordering::Release) + 1;
312        self.waker.wake_up_to(new_gen);
313    }
314
315    /// Inner primitive (sidecar / observability access).
316    pub fn inner(&self) -> &Arc<SharedRWLock> { &self.inner }
317}
318
319/// RAII guard for a held read-lock. Drops to release.
320pub struct BlockingReadGuard<'a> {
321    lock: &'a BlockingRWLock,
322}
323
324impl Drop for BlockingReadGuard<'_> {
325    fn drop(&mut self) {
326        // Manually release the inner read-count. Inner SharedRWLock
327        // does this via its own ReadGuard::drop; we mem::forget'd
328        // that guard so we need to do the equivalent state update.
329        release_read_state(&self.lock.inner);
330        self.lock.signal_unlock();
331    }
332}
333
334/// RAII guard for a held write-lock. Drops to release.
335pub struct BlockingWriteGuard<'a> {
336    lock: &'a BlockingRWLock,
337}
338
339impl Drop for BlockingWriteGuard<'_> {
340    fn drop(&mut self) {
341        release_write_state(&self.lock.inner);
342        self.lock.signal_unlock();
343    }
344}
345
346/// Mirrors `ReadGuard::drop`. Called by `BlockingReadGuard::drop`
347/// after the wrapper's guard captures the unlock event so we can
348/// fire a wake right after the state-release.
349fn release_read_state(lock: &SharedRWLock) {
350    lock.release_read_for_blocking();
351}
352
353fn release_write_state(lock: &SharedRWLock) {
354    lock.release_write_for_blocking();
355}
356
357fn rwlock_path(base: &Path) -> PathBuf {
358    let mut p = base.as_os_str().to_owned();
359    p.push(".rwlock.bin");
360    PathBuf::from(p)
361}
362fn waker_path(base: &Path) -> PathBuf {
363    let mut p = base.as_os_str().to_owned();
364    p.push(".waker.bin");
365    PathBuf::from(p)
366}
367fn wakeup_path(base: &Path) -> PathBuf {
368    let mut p = base.as_os_str().to_owned();
369    p.push(".wakeup.bin");
370    PathBuf::from(p)
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use std::sync::atomic::AtomicU64;
377    use std::thread;
378
379    fn fresh_base() -> PathBuf {
380        let dir = std::env::temp_dir();
381        static N: AtomicU64 = AtomicU64::new(0);
382        let n = N.fetch_add(1, Ordering::Relaxed);
383        dir.join(format!("subetha_brwlock_test_{}_{}", std::process::id(), n))
384    }
385
386    fn cleanup(base: &Path) {
387        for suffix in [".rwlock.bin", ".waker.bin", ".wakeup.bin"] {
388            let mut p = base.as_os_str().to_owned();
389            p.push(suffix);
390            drop(std::fs::remove_file(PathBuf::from(p)));
391        }
392    }
393
394    #[test]
395    fn try_read_then_drop_then_try_write_succeeds() {
396        let base = fresh_base();
397        cleanup(&base);
398        let lock = BlockingRWLock::create(&base).expect("create");
399        let r = lock.try_read_lock().expect("read");
400        drop(r);
401        let w = lock.try_write_lock().expect("write");
402        drop(w);
403        cleanup(&base);
404    }
405
406    #[test]
407    fn write_park_completes_after_reader_releases() {
408        let base = fresh_base();
409        cleanup(&base);
410        let lock = Arc::new(BlockingRWLock::create(&base).expect("create"));
411        let r = lock.try_read_lock().expect("read");
412
413        // Assert the ORDERING property directly: the parked writer
414        // cannot complete before the reader's release. (A fixed
415        // sleep + minimum-elapsed assertion is schedule-sensitive:
416        // under full-suite load the spawned thread can start late
417        // and measure a short block despite behaving correctly.)
418        let lock2 = Arc::clone(&lock);
419        let t = thread::spawn(move || {
420            let _w = lock2.write_park().expect("park-write");
421            Instant::now()
422        });
423        thread::sleep(Duration::from_millis(40));
424        let released_at = Instant::now();
425        drop(r);
426        let completed_at = t.join().unwrap();
427        assert!(completed_at >= released_at,
428                "write_park must not complete before the reader released");
429        cleanup(&base);
430    }
431
432    #[test]
433    fn read_park_timeout_returns_timeout_when_writer_held() {
434        let base = fresh_base();
435        cleanup(&base);
436        let lock = BlockingRWLock::create(&base).expect("create");
437        let _w = lock.try_write_lock().expect("hold-write");
438        let t0 = Instant::now();
439        let err = lock.read_park_timeout(Duration::from_millis(60));
440        assert!(matches!(err, Err(BlockingRWLockError::Timeout)));
441        assert!(t0.elapsed() >= Duration::from_millis(50));
442        cleanup(&base);
443    }
444}