Skip to main content

subetha_cxc/
shared_rw_lock.rs

1//! `SharedRWLock` - cross-process reader-writer lock with writer
2//! priority.
3//!
4//! Multiple concurrent readers OR exactly one writer. When a writer
5//! is waiting, new readers block to prevent writer starvation.
6//!
7//! # State encoding
8//!
9//! ONE AtomicU64 packed:
10//! - bit 63: writer active (1 if a writer holds the lock)
11//! - bits 32-62: writers waiting count (31 bits)
12//! - bits 0-31: reader count (32 bits)
13//!
14//! All transitions are single CAS so observers never see torn state.
15
16use std::fs::{File, OpenOptions};
17use std::mem::size_of;
18use std::path::Path;
19use std::sync::atomic::{AtomicU64, Ordering};
20
21use memmap2::{MmapMut, MmapOptions};
22
23pub const RWLOCK_MAGIC: u64 = 0x4150_5257_4C4F_434B;
24
25/// How long a caller that lost the `create_new` election waits for the winner
26/// to publish the magic before giving up. Bounded so a creator that dies
27/// mid-initialisation surfaces as an error rather than an unbounded spin.
28const CREATE_RACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
29
30const WRITER_BIT: u64 = 1u64 << 63;
31const WAITING_SHIFT: u64 = 32;
32const WAITING_MASK: u64 = 0x7FFF_FFFF << WAITING_SHIFT;
33const READERS_MASK: u64 = 0xFFFF_FFFF;
34
35#[repr(C, align(64))]
36pub struct RWLockHeader {
37    pub magic: u64,
38    pub state: AtomicU64,
39    _pad: [u8; 48],
40}
41
42const _: () = {
43    assert!(size_of::<RWLockHeader>() == 64);
44};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum RWLockError {
48    WouldBlock,
49    LayoutMismatch,
50    IoError(std::io::ErrorKind),
51}
52
53impl From<std::io::Error> for RWLockError {
54    fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
55}
56
57pub struct SharedRWLock {
58    _file: File,
59    mmap: MmapMut,
60    header_sidecar: subetha_core::HandshakeHeader,
61    ring_sidecar: Box<subetha_core::ObservationRing>,
62}
63
64unsafe impl Send for SharedRWLock {}
65unsafe impl Sync for SharedRWLock {}
66
67impl subetha_sidecar::AdaptiveInstance for SharedRWLock {
68    fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
69    fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
70    fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
71        Box::new(subetha_sidecar::NoMigrationPolicy)
72    }
73}
74
75impl SharedRWLock {
76    /// Obtain the lock at `path`, initializing it if it does not yet exist and
77    /// attaching to it if it does. Attaching leaves a held writer flag in
78    /// place. [`reset`](Self::reset) reinitializes.
79    pub fn create(path: impl AsRef<Path>) -> Result<Self, RWLockError> {
80        Self::create_or_open(path)
81    }
82
83    /// Reinitialize the lock at `path`: the header is truncated and zeroed,
84    /// discarding any state a live holder owns. For a caller that owns the
85    /// path.
86    pub fn reset(path: impl AsRef<Path>) -> Result<Self, RWLockError> {
87        let total = size_of::<RWLockHeader>();
88        let file = OpenOptions::new()
89            .read(true).write(true).create(true).truncate(true)
90            .open(path.as_ref())?;
91        file.set_len(total as u64)?;
92        let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
93        let hdr = mmap.as_mut_ptr() as *mut RWLockHeader;
94        unsafe {
95            std::ptr::write_bytes(hdr as *mut u8, 0, total);
96            (*hdr).magic = RWLOCK_MAGIC;
97        }
98        Ok(Self {
99            _file: file, mmap,
100            header_sidecar: subetha_core::HandshakeHeader::new(),
101            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
102        })
103    }
104
105    /// Open the lock at `path`, creating it if it does not exist, without a
106    /// window in which two callers can both create it.
107    ///
108    /// [`create`](Self::create) truncates and zeroes the header, so a second
109    /// caller running it against a live lock clears a writer flag another
110    /// holder owns and mutual exclusion is silently lost. An exists-then-create
111    /// check does not close that: the check and the create are separate steps.
112    /// Here exactly one caller wins an exclusive `create_new` and initialises;
113    /// the rest open and wait for the magic to appear.
114    ///
115    /// Use this for any lock a peer may reach first. [`create`](Self::create)
116    /// stays the right call only when the caller knows it owns the path.
117    pub fn create_or_open(path: impl AsRef<Path>) -> Result<Self, RWLockError> {
118        let total = size_of::<RWLockHeader>();
119        match OpenOptions::new()
120            .read(true)
121            .write(true)
122            .create_new(true)
123            .open(path.as_ref())
124        {
125            Ok(file) => {
126                file.set_len(total as u64)?;
127                let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
128                let hdr = mmap.as_mut_ptr() as *mut RWLockHeader;
129                unsafe {
130                    std::ptr::write_bytes(hdr as *mut u8, 0, total);
131                    // Published last: a peer that opened the file early spins
132                    // on this, so it must not be visible before the zeroing.
133                    (*hdr).magic = RWLOCK_MAGIC;
134                }
135                Ok(Self {
136                    _file: file,
137                    mmap,
138                    header_sidecar: subetha_core::HandshakeHeader::new(),
139                    ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
140                })
141            }
142            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
143                // The winner may not have written the magic yet, and on
144                // Windows the file can be observed at zero length first.
145                let deadline = std::time::Instant::now() + CREATE_RACE_TIMEOUT;
146                loop {
147                    match Self::open(path.as_ref()) {
148                        Ok(l) => return Ok(l),
149                        Err(RWLockError::LayoutMismatch) | Err(RWLockError::IoError(_))
150                            if std::time::Instant::now() < deadline =>
151                        {
152                            std::thread::yield_now();
153                        }
154                        Err(other) => return Err(other),
155                    }
156                }
157            }
158            Err(e) => Err(e.into()),
159        }
160    }
161
162    pub fn open(path: impl AsRef<Path>) -> Result<Self, RWLockError> {
163        let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
164        if file.metadata()?.len() < size_of::<RWLockHeader>() as u64 {
165            return Err(RWLockError::LayoutMismatch);
166        }
167        let mmap = unsafe {
168            MmapOptions::new().len(size_of::<RWLockHeader>()).map_mut(&file)?
169        };
170        let hdr = unsafe { &*(mmap.as_ptr() as *const RWLockHeader) };
171        if hdr.magic != RWLOCK_MAGIC {
172            return Err(RWLockError::LayoutMismatch);
173        }
174        Ok(Self {
175            _file: file, mmap,
176            header_sidecar: subetha_core::HandshakeHeader::new(),
177            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
178        })
179    }
180
181    fn state(&self) -> &AtomicU64 {
182        unsafe { &(*(self.mmap.as_ptr() as *const RWLockHeader)).state }
183    }
184
185    /// Try to acquire a read lock without blocking.
186    pub fn try_read_lock(&self) -> Result<ReadGuard<'_>, RWLockError> {
187        let r = self.try_read_lock_inner();
188        self.ring_sidecar.push_op(
189            crate::sidecar_ops::rw_lock::OP_TRY_READ,
190            if r.is_err() { 1 } else { 0 },
191        );
192        r
193    }
194
195    fn try_read_lock_inner(&self) -> Result<ReadGuard<'_>, RWLockError> {
196        loop {
197            let s = self.state().load(Ordering::Acquire);
198            let writer_active = (s & WRITER_BIT) != 0;
199            let writers_waiting = (s & WAITING_MASK) >> WAITING_SHIFT;
200            if writer_active || writers_waiting > 0 {
201                return Err(RWLockError::WouldBlock);
202            }
203            let readers = s & READERS_MASK;
204            let new = (s & !READERS_MASK) | (readers + 1);
205            if self.state().compare_exchange(
206                s, new, Ordering::AcqRel, Ordering::Acquire,
207            ).is_ok() {
208                return Ok(ReadGuard { lock: self });
209            }
210        }
211    }
212
213    /// Acquire a read lock, blocking with backoff until available.
214    /// Writer-priority: blocks if any writer is active OR waiting.
215    pub fn read_lock(&self) -> ReadGuard<'_> {
216        let mut spins = 0u32;
217        loop {
218            if let Ok(g) = self.try_read_lock_inner() {
219                self.ring_sidecar.push_op(
220                    crate::sidecar_ops::rw_lock::OP_READ,
221                    if spins > 0 { 1 } else { 0 }, // contention
222                );
223                return g;
224            }
225            spins += 1;
226            if spins < 32 {
227                std::hint::spin_loop();
228            } else if spins < 256 {
229                std::thread::yield_now();
230            } else {
231                std::thread::sleep(std::time::Duration::from_micros(50));
232            }
233        }
234    }
235
236    /// Try to acquire a write lock without blocking.
237    pub fn try_write_lock(&self) -> Result<WriteGuard<'_>, RWLockError> {
238        let r = self.try_write_lock_inner();
239        self.ring_sidecar.push_op(
240            crate::sidecar_ops::rw_lock::OP_TRY_WRITE,
241            if r.is_err() { 1 } else { 0 },
242        );
243        r
244    }
245
246    fn try_write_lock_inner(&self) -> Result<WriteGuard<'_>, RWLockError> {
247        loop {
248            let s = self.state().load(Ordering::Acquire);
249            let writer_active = (s & WRITER_BIT) != 0;
250            let readers = s & READERS_MASK;
251            if writer_active || readers > 0 {
252                return Err(RWLockError::WouldBlock);
253            }
254            let new = (s & !WRITER_BIT) | WRITER_BIT;
255            if self.state().compare_exchange(
256                s, new, Ordering::AcqRel, Ordering::Acquire,
257            ).is_ok() {
258                return Ok(WriteGuard { lock: self });
259            }
260        }
261    }
262
263    /// Acquire a write lock, blocking until available. Registers
264    /// as "waiting" so new readers will block.
265    pub fn write_lock(&self) -> WriteGuard<'_> {
266        // Register as waiting.
267        self.state().fetch_add(1u64 << WAITING_SHIFT, Ordering::AcqRel);
268        let mut spins = 0u32;
269        loop {
270            let s = self.state().load(Ordering::Acquire);
271            let writer_active = (s & WRITER_BIT) != 0;
272            let readers = s & READERS_MASK;
273            if !writer_active && readers == 0 {
274                // Try to claim: set writer bit + decrement waiting.
275                let new = (s & READERS_MASK) | WRITER_BIT
276                    | ((((s & WAITING_MASK) >> WAITING_SHIFT) - 1) << WAITING_SHIFT);
277                if self.state().compare_exchange(
278                    s, new, Ordering::AcqRel, Ordering::Acquire,
279                ).is_ok() {
280                    self.ring_sidecar.push_op(
281                        crate::sidecar_ops::rw_lock::OP_WRITE,
282                        if spins > 0 { 1 } else { 0 }, // contention
283                    );
284                    return WriteGuard { lock: self };
285                }
286            }
287            spins += 1;
288            if spins < 32 {
289                std::hint::spin_loop();
290            } else if spins < 256 {
291                std::thread::yield_now();
292            } else {
293                std::thread::sleep(std::time::Duration::from_micros(50));
294            }
295        }
296    }
297
298    /// Number of active readers (observational; may race).
299    pub fn reader_count(&self) -> u32 {
300        (self.state().load(Ordering::Acquire) & READERS_MASK) as u32
301    }
302
303    /// True if a writer currently holds the lock.
304    pub fn has_writer(&self) -> bool {
305        (self.state().load(Ordering::Acquire) & WRITER_BIT) != 0
306    }
307
308    /// Number of writers currently waiting for the lock.
309    pub fn waiting_writers(&self) -> u32 {
310        ((self.state().load(Ordering::Acquire) & WAITING_MASK) >> WAITING_SHIFT) as u32
311    }
312
313    /// Release one reader. Internal; called by ReadGuard::drop.
314    /// Defensive: checks that the reader count is positive before
315    /// decrementing. In debug builds this panics on protocol
316    /// violation (release without acquire); in release builds it
317    /// silently no-ops to avoid underflow corruption.
318    fn release_read(&self) {
319        loop {
320            let s = self.state().load(Ordering::Acquire);
321            let readers = s & READERS_MASK;
322            debug_assert!(
323                readers > 0,
324                "SharedRWLock::release_read called when reader count is 0 - \
325                 indicates a protocol violation (double-release or release \
326                 without acquire). The lock counter will not be decremented.",
327            );
328            if readers == 0 { return; }
329            let new = (s & !READERS_MASK) | (readers - 1);
330            if self.state().compare_exchange(
331                s, new, Ordering::AcqRel, Ordering::Acquire,
332            ).is_ok() {
333                return;
334            }
335        }
336    }
337
338    /// Release the writer. Internal; called by WriteGuard::drop.
339    /// Defensive: checks that a writer is actually active before
340    /// clearing. In debug builds this panics on protocol violation.
341    fn release_write(&self) {
342        let prev = self.state().fetch_and(!WRITER_BIT, Ordering::AcqRel);
343        debug_assert!(
344            (prev & WRITER_BIT) != 0,
345            "SharedRWLock::release_write called when no writer holds the lock - \
346             indicates a protocol violation (double-release or release without \
347             acquire).",
348        );
349    }
350
351    /// Public hook for the `BlockingRWLock` wrapper to mirror the
352    /// inner `ReadGuard::drop` semantics after the wrapper's own
353    /// guard runs (the wrapper `mem::forget`s the inner guard so it
354    /// can interleave a wake call between the state release and the
355    /// guard's destructor).
356    pub fn release_read_for_blocking(&self) { self.release_read(); }
357
358    /// Public hook for the `BlockingRWLock` wrapper; mirror of
359    /// `WriteGuard::drop`.
360    pub fn release_write_for_blocking(&self) { self.release_write(); }
361
362    pub fn flush(&self) -> Result<(), RWLockError> {
363        self.mmap.flush()?;
364        Ok(())
365    }
366    pub fn flush_async(&self) -> Result<(), RWLockError> {
367        self.mmap.flush_async()?;
368        Ok(())
369    }
370}
371
372pub struct ReadGuard<'a> { lock: &'a SharedRWLock }
373impl Drop for ReadGuard<'_> {
374    fn drop(&mut self) { self.lock.release_read(); }
375}
376
377pub struct WriteGuard<'a> { lock: &'a SharedRWLock }
378impl Drop for WriteGuard<'_> {
379    fn drop(&mut self) { self.lock.release_write(); }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use std::sync::atomic::{AtomicU32, Ordering as O};
386    use std::sync::Arc;
387    use std::thread;
388
389    fn tmp(name: &str) -> std::path::PathBuf {
390        let mut p = std::env::temp_dir();
391        let pid = std::process::id();
392        p.push(format!("subetha-rwlock-{name}-{pid}.bin"));
393        p
394    }
395
396    /// Racing callers on one path must all reach the same lock, and a caller
397    /// arriving while another holds the write lock must not clear it.
398    ///
399    /// `create` truncates and zeroes the header, so a second caller running it
400    /// against a live lock drops a held writer flag and mutual exclusion is
401    /// gone with no error anywhere. `create_or_open` elects one creator.
402    #[test]
403    fn create_or_open_racing_callers_do_not_clear_a_held_writer() {
404        let p = tmp("race");
405        std::fs::remove_file(&p).ok();
406
407        let holder = SharedRWLock::create_or_open(&p).unwrap();
408        let guard = holder.try_write_lock().expect("uncontended write lock");
409        assert!(holder.has_writer());
410
411        // Eight peers arrive on the same path while the write lock is held.
412        let path = Arc::new(p.clone());
413        let cleared = Arc::new(AtomicU32::new(0));
414        let mut hs = Vec::new();
415        for _ in 0..8 {
416            let path = Arc::clone(&path);
417            let cleared = Arc::clone(&cleared);
418            hs.push(thread::spawn(move || {
419                let l = SharedRWLock::create_or_open(&*path).expect("open existing");
420                if !l.has_writer() {
421                    cleared.fetch_add(1, O::Relaxed);
422                }
423            }));
424        }
425        for h in hs {
426            h.join().unwrap();
427        }
428
429        assert_eq!(
430            cleared.load(O::Relaxed),
431            0,
432            "a concurrent create_or_open zeroed a writer flag another holder owned",
433        );
434        assert!(holder.has_writer(), "the holder lost its own write lock");
435        drop(guard);
436        std::fs::remove_file(&p).ok();
437    }
438
439    #[test]
440    fn create_initial_state_is_idle() {
441        let p = tmp("init");
442        let l = SharedRWLock::create(&p).unwrap();
443        assert_eq!(l.reader_count(), 0);
444        assert!(!l.has_writer());
445        assert_eq!(l.waiting_writers(), 0);
446        std::fs::remove_file(&p).ok();
447    }
448
449    #[test]
450    fn try_read_succeeds_when_idle() {
451        let p = tmp("try-read");
452        let l = SharedRWLock::create(&p).unwrap();
453        let g = l.try_read_lock().unwrap();
454        assert_eq!(l.reader_count(), 1);
455        drop(g);
456        assert_eq!(l.reader_count(), 0);
457        std::fs::remove_file(&p).ok();
458    }
459
460    #[test]
461    fn try_write_succeeds_when_idle() {
462        let p = tmp("try-write");
463        let l = SharedRWLock::create(&p).unwrap();
464        let g = l.try_write_lock().unwrap();
465        assert!(l.has_writer());
466        drop(g);
467        assert!(!l.has_writer());
468        std::fs::remove_file(&p).ok();
469    }
470
471    #[test]
472    fn multiple_readers_coexist() {
473        let p = tmp("multi-read");
474        let l = SharedRWLock::create(&p).unwrap();
475        let g1 = l.try_read_lock().unwrap();
476        let g2 = l.try_read_lock().unwrap();
477        let g3 = l.try_read_lock().unwrap();
478        assert_eq!(l.reader_count(), 3);
479        drop(g1); drop(g2); drop(g3);
480        assert_eq!(l.reader_count(), 0);
481        std::fs::remove_file(&p).ok();
482    }
483
484    #[test]
485    fn writer_excludes_readers() {
486        let p = tmp("w-excl-r");
487        let l = SharedRWLock::create(&p).unwrap();
488        let _w = l.try_write_lock().unwrap();
489        assert_eq!(l.try_read_lock().err(), Some(RWLockError::WouldBlock));
490        std::fs::remove_file(&p).ok();
491    }
492
493    #[test]
494    fn reader_excludes_writer() {
495        let p = tmp("r-excl-w");
496        let l = SharedRWLock::create(&p).unwrap();
497        let _r = l.try_read_lock().unwrap();
498        assert_eq!(l.try_write_lock().err(), Some(RWLockError::WouldBlock));
499        std::fs::remove_file(&p).ok();
500    }
501
502    #[test]
503    fn writer_excludes_writer() {
504        let p = tmp("w-excl-w");
505        let l = SharedRWLock::create(&p).unwrap();
506        let _w = l.try_write_lock().unwrap();
507        assert_eq!(l.try_write_lock().err(), Some(RWLockError::WouldBlock));
508        std::fs::remove_file(&p).ok();
509    }
510
511    #[test]
512    fn write_lock_blocks_until_readers_drop() {
513        // Clean pattern: spawn a reader thread that holds its guard
514        // for a known duration. Main thread spawns a writer that
515        // must block until the reader's guard drops. No unsafe
516        // ptr::read; the reader's guard lifetime is tied to its
517        // thread's scope.
518        let p = tmp("w-blocks");
519        let l = Arc::new(SharedRWLock::create(&p).unwrap());
520        let l_reader = l.clone();
521        let reader_done = Arc::new(AtomicU32::new(0));
522        let reader_done_clone = reader_done.clone();
523        let reader = thread::spawn(move || {
524            let _g = l_reader.read_lock();
525            std::thread::sleep(std::time::Duration::from_millis(30));
526            reader_done_clone.store(1, O::Release);
527            // Guard drops here, releasing the lock.
528        });
529        // Wait (bounded) for the reader thread to acquire; a fixed
530        // sleep races the scheduler under full-suite load.
531        let acquire_deadline = std::time::Instant::now()
532            + std::time::Duration::from_secs(5);
533        while l.reader_count() != 1
534            && std::time::Instant::now() < acquire_deadline
535        {
536            std::thread::yield_now();
537        }
538        assert_eq!(l.reader_count(), 1);
539
540        let l_writer = l.clone();
541        let writer_started = std::time::Instant::now();
542        let writer = thread::spawn(move || {
543            let _g = l_writer.write_lock();
544            writer_started.elapsed()
545        });
546
547        let elapsed = writer.join().unwrap();
548        reader.join().unwrap();
549        // Writer should have blocked at least until reader finished
550        // (which is ~30ms - 5ms from when writer was spawned = ~25ms).
551        assert!(
552            elapsed >= std::time::Duration::from_millis(15),
553            "writer should have blocked for the reader's hold time, got {elapsed:?}",
554        );
555        assert_eq!(reader_done.load(O::Acquire), 1);
556        std::fs::remove_file(&p).ok();
557    }
558
559    #[test]
560    fn writer_priority_blocks_new_readers() {
561        // When a writer is waiting, new try_read should fail
562        // (writer priority).
563        let p = tmp("w-priority");
564        let l = SharedRWLock::create(&p).unwrap();
565        // Simulate a waiting writer by bumping the waiting count
566        // directly (real writers do this in write_lock).
567        l.state().fetch_add(1u64 << WAITING_SHIFT, Ordering::AcqRel);
568        assert_eq!(l.try_read_lock().err(), Some(RWLockError::WouldBlock));
569        // Clean up the bumped count for the file teardown.
570        l.state().fetch_sub(1u64 << WAITING_SHIFT, Ordering::AcqRel);
571        std::fs::remove_file(&p).ok();
572    }
573
574    #[test]
575    fn concurrent_readers_all_succeed() {
576        let p = tmp("concurrent-r");
577        let l = Arc::new(SharedRWLock::create(&p).unwrap());
578        let n = 8;
579        let count = Arc::new(AtomicU32::new(0));
580        let mut handles = vec![];
581        for _ in 0..n {
582            let l = l.clone();
583            let count = count.clone();
584            handles.push(thread::spawn(move || {
585                let _g = l.read_lock();
586                count.fetch_add(1, O::AcqRel);
587                std::thread::sleep(std::time::Duration::from_millis(5));
588            }));
589        }
590        for h in handles { h.join().unwrap(); }
591        assert_eq!(count.load(O::Acquire), n);
592        std::fs::remove_file(&p).ok();
593    }
594
595    #[test]
596    fn cross_handle_visibility() {
597        let p = tmp("cross-handle");
598        let w = SharedRWLock::create(&p).unwrap();
599        let r = SharedRWLock::open(&p).unwrap();
600        let _g = w.try_read_lock().unwrap();
601        // Reader handle sees the same state.
602        assert_eq!(r.reader_count(), 1);
603        assert_eq!(r.try_write_lock().err(), Some(RWLockError::WouldBlock));
604        std::fs::remove_file(&p).ok();
605    }
606
607    #[test]
608    fn writer_then_reader_serialized() {
609        let p = tmp("w-then-r");
610        let l = SharedRWLock::create(&p).unwrap();
611        {
612            let _w = l.try_write_lock().unwrap();
613        }
614        // After writer drops, reader can acquire.
615        let _r = l.try_read_lock().unwrap();
616        std::fs::remove_file(&p).ok();
617    }
618}