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    fn create_file(path: &Path) -> Result<Self, BlockingRWLockError> {
83        let file = OpenOptions::new()
84            .read(true).write(true).create(true).truncate(true)
85            .open(path)?;
86        file.set_len(WAKEUP_REGION_SIZE as u64)?;
87        let mut mmap = unsafe { MmapOptions::new().len(WAKEUP_REGION_SIZE).map_mut(&file)? };
88        let base = mmap.as_mut_ptr();
89        unsafe {
90            (base as *mut u64).write(WAKEUP_MAGIC);
91            (base.add(WAKEUP_OFFSET) as *mut AtomicU64).write(AtomicU64::new(0));
92        }
93        let ptr = unsafe { base.add(WAKEUP_OFFSET) as *const AtomicU64 };
94        Ok(Self { backing: WakeupBacking::File(file, mmap), ptr })
95    }
96
97    fn open_file(path: &Path) -> Result<Self, BlockingRWLockError> {
98        let file = OpenOptions::new().read(true).write(true).open(path)?;
99        if (file.metadata()?.len() as usize) < WAKEUP_REGION_SIZE {
100            return Err(BlockingRWLockError::LayoutMismatch);
101        }
102        let mut mmap = unsafe { MmapOptions::new().len(WAKEUP_REGION_SIZE).map_mut(&file)? };
103        let base = mmap.as_mut_ptr();
104        let magic = unsafe { (base as *const u64).read() };
105        if magic != WAKEUP_MAGIC {
106            return Err(BlockingRWLockError::LayoutMismatch);
107        }
108        let ptr = unsafe { base.add(WAKEUP_OFFSET) as *const AtomicU64 };
109        Ok(Self { backing: WakeupBacking::File(file, mmap), ptr })
110    }
111
112    #[inline]
113    fn atom(&self) -> &AtomicU64 { unsafe { &*self.ptr } }
114}
115
116/// Cross-process reader-writer lock with kernel-park slow path.
117pub struct BlockingRWLock {
118    inner: Arc<SharedRWLock>,
119    waker: Arc<CrossProcessWaker>,
120    wakeup: Arc<WakeupAtom>,
121}
122
123const PRE_PARK_SPIN: u32 = 32;
124
125impl BlockingRWLock {
126    /// Create a blocking rwlock. Lays out three files under one
127    /// base path: `<base>.rwlock.bin` for the state atom,
128    /// `<base>.waker.bin` for the waker slots, `<base>.wakeup.bin`
129    /// for the magic + generation atom.
130    pub fn create(base_path: impl AsRef<Path>) -> Result<Self, BlockingRWLockError> {
131        let base = base_path.as_ref();
132        let inner = SharedRWLock::create(rwlock_path(base))?;
133        let waker = CrossProcessWaker::create(waker_path(base), MAX_WAITERS_DEFAULT)?;
134        let wakeup = WakeupAtom::create_file(&wakeup_path(base))?;
135        Ok(Self {
136            inner: Arc::new(inner),
137            waker: Arc::new(waker),
138            wakeup: Arc::new(wakeup),
139        })
140    }
141
142    /// Open an existing blocking rwlock.
143    pub fn open(base_path: impl AsRef<Path>) -> Result<Self, BlockingRWLockError> {
144        let base = base_path.as_ref();
145        let inner = SharedRWLock::open(rwlock_path(base))?;
146        let waker = CrossProcessWaker::open(waker_path(base), MAX_WAITERS_DEFAULT)?;
147        let wakeup = WakeupAtom::open_file(&wakeup_path(base))?;
148        Ok(Self {
149            inner: Arc::new(inner),
150            waker: Arc::new(waker),
151            wakeup: Arc::new(wakeup),
152        })
153    }
154
155    /// Non-blocking read-lock attempt.
156    pub fn try_read_lock(&self) -> Result<BlockingReadGuard<'_>, BlockingRWLockError> {
157        match self.inner.try_read_lock() {
158            Ok(g) => {
159                std::mem::forget(g);
160                Ok(BlockingReadGuard { lock: self })
161            }
162            Err(e) => Err(BlockingRWLockError::Lock(e)),
163        }
164    }
165
166    /// Non-blocking write-lock attempt.
167    pub fn try_write_lock(&self) -> Result<BlockingWriteGuard<'_>, BlockingRWLockError> {
168        match self.inner.try_write_lock() {
169            Ok(g) => {
170                std::mem::forget(g);
171                Ok(BlockingWriteGuard { lock: self })
172            }
173            Err(e) => Err(BlockingRWLockError::Lock(e)),
174        }
175    }
176
177    /// Read-lock with kernel-park slow path.
178    pub fn read_park(&self) -> Result<BlockingReadGuard<'_>, BlockingRWLockError> {
179        loop {
180            if let Ok(g) = self.inner.try_read_lock() {
181                std::mem::forget(g);
182                return Ok(BlockingReadGuard { lock: self });
183            }
184            for _ in 0..PRE_PARK_SPIN {
185                if let Ok(g) = self.inner.try_read_lock() {
186                    std::mem::forget(g);
187                    return Ok(BlockingReadGuard { lock: self });
188                }
189                std::hint::spin_loop();
190            }
191            let snapshot = self.wakeup.atom().load(Ordering::Acquire);
192            let token = self.waker.try_park(snapshot + 1)?;
193            if let Ok(g) = self.inner.try_read_lock() {
194                self.waker.release(token);
195                std::mem::forget(g);
196                return Ok(BlockingReadGuard { lock: self });
197            }
198            self.waker.wait(token, None)?;
199        }
200    }
201
202    /// Read-lock with bounded wait.
203    pub fn read_park_timeout(
204        &self,
205        timeout: Duration,
206    ) -> Result<BlockingReadGuard<'_>, BlockingRWLockError> {
207        let deadline = Instant::now() + timeout;
208        loop {
209            if let Ok(g) = self.inner.try_read_lock() {
210                std::mem::forget(g);
211                return Ok(BlockingReadGuard { lock: self });
212            }
213            for _ in 0..PRE_PARK_SPIN {
214                if let Ok(g) = self.inner.try_read_lock() {
215                    std::mem::forget(g);
216                    return Ok(BlockingReadGuard { lock: self });
217                }
218                std::hint::spin_loop();
219            }
220            let snapshot = self.wakeup.atom().load(Ordering::Acquire);
221            let token = self.waker.try_park(snapshot + 1)?;
222            if let Ok(g) = self.inner.try_read_lock() {
223                self.waker.release(token);
224                std::mem::forget(g);
225                return Ok(BlockingReadGuard { lock: self });
226            }
227            let now = Instant::now();
228            if now >= deadline {
229                self.waker.release(token);
230                return Err(BlockingRWLockError::Timeout);
231            }
232            let remaining = deadline - now;
233            match self.waker.wait(token, Some(remaining)) {
234                Ok(()) => continue,
235                Err(WakerError::Timeout) => return Err(BlockingRWLockError::Timeout),
236                Err(e) => return Err(BlockingRWLockError::Waker(e)),
237            }
238        }
239    }
240
241    /// Write-lock with kernel-park slow path.
242    pub fn write_park(&self) -> Result<BlockingWriteGuard<'_>, BlockingRWLockError> {
243        loop {
244            if let Ok(g) = self.inner.try_write_lock() {
245                std::mem::forget(g);
246                return Ok(BlockingWriteGuard { lock: self });
247            }
248            for _ in 0..PRE_PARK_SPIN {
249                if let Ok(g) = self.inner.try_write_lock() {
250                    std::mem::forget(g);
251                    return Ok(BlockingWriteGuard { lock: self });
252                }
253                std::hint::spin_loop();
254            }
255            let snapshot = self.wakeup.atom().load(Ordering::Acquire);
256            let token = self.waker.try_park(snapshot + 1)?;
257            if let Ok(g) = self.inner.try_write_lock() {
258                self.waker.release(token);
259                std::mem::forget(g);
260                return Ok(BlockingWriteGuard { lock: self });
261            }
262            self.waker.wait(token, None)?;
263        }
264    }
265
266    /// Write-lock with bounded wait.
267    pub fn write_park_timeout(
268        &self,
269        timeout: Duration,
270    ) -> Result<BlockingWriteGuard<'_>, BlockingRWLockError> {
271        let deadline = Instant::now() + timeout;
272        loop {
273            if let Ok(g) = self.inner.try_write_lock() {
274                std::mem::forget(g);
275                return Ok(BlockingWriteGuard { lock: self });
276            }
277            for _ in 0..PRE_PARK_SPIN {
278                if let Ok(g) = self.inner.try_write_lock() {
279                    std::mem::forget(g);
280                    return Ok(BlockingWriteGuard { lock: self });
281                }
282                std::hint::spin_loop();
283            }
284            let snapshot = self.wakeup.atom().load(Ordering::Acquire);
285            let token = self.waker.try_park(snapshot + 1)?;
286            if let Ok(g) = self.inner.try_write_lock() {
287                self.waker.release(token);
288                std::mem::forget(g);
289                return Ok(BlockingWriteGuard { lock: self });
290            }
291            let now = Instant::now();
292            if now >= deadline {
293                self.waker.release(token);
294                return Err(BlockingRWLockError::Timeout);
295            }
296            let remaining = deadline - now;
297            match self.waker.wait(token, Some(remaining)) {
298                Ok(()) => continue,
299                Err(WakerError::Timeout) => return Err(BlockingRWLockError::Timeout),
300                Err(e) => return Err(BlockingRWLockError::Waker(e)),
301            }
302        }
303    }
304
305    /// Bump wakeup-generation + wake every parker. Called from the
306    /// guard Drop paths after the inner lock is released.
307    fn signal_unlock(&self) {
308        let new_gen = self.wakeup.atom().fetch_add(1, Ordering::Release) + 1;
309        self.waker.wake_up_to(new_gen);
310    }
311
312    /// Inner primitive (sidecar / observability access).
313    pub fn inner(&self) -> &Arc<SharedRWLock> { &self.inner }
314}
315
316/// RAII guard for a held read-lock. Drops to release.
317pub struct BlockingReadGuard<'a> {
318    lock: &'a BlockingRWLock,
319}
320
321impl Drop for BlockingReadGuard<'_> {
322    fn drop(&mut self) {
323        // Manually release the inner read-count. Inner SharedRWLock
324        // does this via its own ReadGuard::drop; we mem::forget'd
325        // that guard so we need to do the equivalent state update.
326        release_read_state(&self.lock.inner);
327        self.lock.signal_unlock();
328    }
329}
330
331/// RAII guard for a held write-lock. Drops to release.
332pub struct BlockingWriteGuard<'a> {
333    lock: &'a BlockingRWLock,
334}
335
336impl Drop for BlockingWriteGuard<'_> {
337    fn drop(&mut self) {
338        release_write_state(&self.lock.inner);
339        self.lock.signal_unlock();
340    }
341}
342
343/// Mirrors `ReadGuard::drop`. Called by `BlockingReadGuard::drop`
344/// after the wrapper's guard captures the unlock event so we can
345/// fire a wake right after the state-release.
346fn release_read_state(lock: &SharedRWLock) {
347    lock.release_read_for_blocking();
348}
349
350fn release_write_state(lock: &SharedRWLock) {
351    lock.release_write_for_blocking();
352}
353
354fn rwlock_path(base: &Path) -> PathBuf {
355    let mut p = base.as_os_str().to_owned();
356    p.push(".rwlock.bin");
357    PathBuf::from(p)
358}
359fn waker_path(base: &Path) -> PathBuf {
360    let mut p = base.as_os_str().to_owned();
361    p.push(".waker.bin");
362    PathBuf::from(p)
363}
364fn wakeup_path(base: &Path) -> PathBuf {
365    let mut p = base.as_os_str().to_owned();
366    p.push(".wakeup.bin");
367    PathBuf::from(p)
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use std::sync::atomic::AtomicU64;
374    use std::thread;
375
376    fn fresh_base() -> PathBuf {
377        let dir = std::env::temp_dir();
378        static N: AtomicU64 = AtomicU64::new(0);
379        let n = N.fetch_add(1, Ordering::Relaxed);
380        dir.join(format!("subetha_brwlock_test_{}_{}", std::process::id(), n))
381    }
382
383    fn cleanup(base: &Path) {
384        for suffix in [".rwlock.bin", ".waker.bin", ".wakeup.bin"] {
385            let mut p = base.as_os_str().to_owned();
386            p.push(suffix);
387            drop(std::fs::remove_file(PathBuf::from(p)));
388        }
389    }
390
391    #[test]
392    fn try_read_then_drop_then_try_write_succeeds() {
393        let base = fresh_base();
394        cleanup(&base);
395        let lock = BlockingRWLock::create(&base).expect("create");
396        let r = lock.try_read_lock().expect("read");
397        drop(r);
398        let w = lock.try_write_lock().expect("write");
399        drop(w);
400        cleanup(&base);
401    }
402
403    #[test]
404    fn write_park_completes_after_reader_releases() {
405        let base = fresh_base();
406        cleanup(&base);
407        let lock = Arc::new(BlockingRWLock::create(&base).expect("create"));
408        let r = lock.try_read_lock().expect("read");
409
410        // Assert the ORDERING property directly: the parked writer
411        // cannot complete before the reader's release. (A fixed
412        // sleep + minimum-elapsed assertion is schedule-sensitive:
413        // under full-suite load the spawned thread can start late
414        // and measure a short block despite behaving correctly.)
415        let lock2 = Arc::clone(&lock);
416        let t = thread::spawn(move || {
417            let _w = lock2.write_park().expect("park-write");
418            Instant::now()
419        });
420        thread::sleep(Duration::from_millis(40));
421        let released_at = Instant::now();
422        drop(r);
423        let completed_at = t.join().unwrap();
424        assert!(completed_at >= released_at,
425                "write_park must not complete before the reader released");
426        cleanup(&base);
427    }
428
429    #[test]
430    fn read_park_timeout_returns_timeout_when_writer_held() {
431        let base = fresh_base();
432        cleanup(&base);
433        let lock = BlockingRWLock::create(&base).expect("create");
434        let _w = lock.try_write_lock().expect("hold-write");
435        let t0 = Instant::now();
436        let err = lock.read_park_timeout(Duration::from_millis(60));
437        assert!(matches!(err, Err(BlockingRWLockError::Timeout)));
438        assert!(t0.elapsed() >= Duration::from_millis(50));
439        cleanup(&base);
440    }
441}