1use 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
25const 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 pub fn create(path: impl AsRef<Path>) -> Result<Self, RWLockError> {
86 Self::create_or_open(path)
87 }
88
89 pub fn reset(path: impl AsRef<Path>) -> Result<Self, RWLockError> {
93 let total = size_of::<RWLockHeader>();
94 let file = OpenOptions::new()
95 .read(true).write(true).create(true).truncate(true)
96 .open(path.as_ref())?;
97 file.set_len(total as u64)?;
98 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
99 let hdr = mmap.as_mut_ptr() as *mut RWLockHeader;
100 unsafe {
101 std::ptr::write_bytes(hdr as *mut u8, 0, total);
102 (*hdr).magic = RWLOCK_MAGIC;
103 }
104 Ok(Self {
105 _file: file, mmap,
106 header_sidecar: subetha_core::HandshakeHeader::new(),
107 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
108 })
109 }
110
111 pub fn create_or_open(path: impl AsRef<Path>) -> Result<Self, RWLockError> {
124 let total = size_of::<RWLockHeader>();
125 match OpenOptions::new()
126 .read(true)
127 .write(true)
128 .create_new(true)
129 .open(path.as_ref())
130 {
131 Ok(file) => {
132 file.set_len(total as u64)?;
133 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
134 let hdr = mmap.as_mut_ptr() as *mut RWLockHeader;
135 unsafe {
136 std::ptr::write_bytes(hdr as *mut u8, 0, total);
137 (*hdr).magic = RWLOCK_MAGIC;
140 }
141 Ok(Self {
142 _file: file,
143 mmap,
144 header_sidecar: subetha_core::HandshakeHeader::new(),
145 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
146 })
147 }
148 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
149 let deadline = std::time::Instant::now() + CREATE_RACE_TIMEOUT;
152 loop {
153 match Self::open(path.as_ref()) {
154 Ok(l) => return Ok(l),
155 Err(RWLockError::LayoutMismatch) | Err(RWLockError::IoError(_))
156 if std::time::Instant::now() < deadline =>
157 {
158 std::thread::yield_now();
159 }
160 Err(other) => return Err(other),
161 }
162 }
163 }
164 Err(e) => Err(e.into()),
165 }
166 }
167
168 pub fn open(path: impl AsRef<Path>) -> Result<Self, RWLockError> {
169 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
170 if file.metadata()?.len() < size_of::<RWLockHeader>() as u64 {
171 return Err(RWLockError::LayoutMismatch);
172 }
173 let mmap = unsafe {
174 MmapOptions::new().len(size_of::<RWLockHeader>()).map_mut(&file)?
175 };
176 let hdr = unsafe { &*(mmap.as_ptr() as *const RWLockHeader) };
177 if hdr.magic != RWLOCK_MAGIC {
178 return Err(RWLockError::LayoutMismatch);
179 }
180 Ok(Self {
181 _file: file, mmap,
182 header_sidecar: subetha_core::HandshakeHeader::new(),
183 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
184 })
185 }
186
187 fn state(&self) -> &AtomicU64 {
188 unsafe { &(*(self.mmap.as_ptr() as *const RWLockHeader)).state }
189 }
190
191 pub fn try_read_lock(&self) -> Result<ReadGuard<'_>, RWLockError> {
193 let r = self.try_read_lock_inner();
194 self.ring_sidecar.push_op(
195 crate::sidecar_ops::rw_lock::OP_TRY_READ,
196 if r.is_err() { 1 } else { 0 },
197 );
198 r
199 }
200
201 fn try_read_lock_inner(&self) -> Result<ReadGuard<'_>, RWLockError> {
202 loop {
203 let s = self.state().load(Ordering::Acquire);
204 let writer_active = (s & WRITER_BIT) != 0;
205 let writers_waiting = (s & WAITING_MASK) >> WAITING_SHIFT;
206 if writer_active || writers_waiting > 0 {
207 return Err(RWLockError::WouldBlock);
208 }
209 let readers = s & READERS_MASK;
210 let new = (s & !READERS_MASK) | (readers + 1);
211 if self.state().compare_exchange(
212 s, new, Ordering::AcqRel, Ordering::Acquire,
213 ).is_ok() {
214 return Ok(ReadGuard { lock: self });
215 }
216 }
217 }
218
219 pub fn read_lock(&self) -> ReadGuard<'_> {
222 let mut spins = 0u32;
223 loop {
224 if let Ok(g) = self.try_read_lock_inner() {
225 self.ring_sidecar.push_op(
226 crate::sidecar_ops::rw_lock::OP_READ,
227 if spins > 0 { 1 } else { 0 }, );
229 return g;
230 }
231 spins += 1;
232 if spins < 32 {
233 std::hint::spin_loop();
234 } else if spins < 256 {
235 std::thread::yield_now();
236 } else {
237 std::thread::sleep(std::time::Duration::from_micros(50));
238 }
239 }
240 }
241
242 pub fn try_write_lock(&self) -> Result<WriteGuard<'_>, RWLockError> {
244 let r = self.try_write_lock_inner();
245 self.ring_sidecar.push_op(
246 crate::sidecar_ops::rw_lock::OP_TRY_WRITE,
247 if r.is_err() { 1 } else { 0 },
248 );
249 r
250 }
251
252 fn try_write_lock_inner(&self) -> Result<WriteGuard<'_>, RWLockError> {
253 loop {
254 let s = self.state().load(Ordering::Acquire);
255 let writer_active = (s & WRITER_BIT) != 0;
256 let readers = s & READERS_MASK;
257 if writer_active || readers > 0 {
258 return Err(RWLockError::WouldBlock);
259 }
260 let new = (s & !WRITER_BIT) | WRITER_BIT;
261 if self.state().compare_exchange(
262 s, new, Ordering::AcqRel, Ordering::Acquire,
263 ).is_ok() {
264 return Ok(WriteGuard { lock: self });
265 }
266 }
267 }
268
269 pub fn write_lock(&self) -> WriteGuard<'_> {
272 self.state().fetch_add(1u64 << WAITING_SHIFT, Ordering::AcqRel);
274 let mut spins = 0u32;
275 loop {
276 let s = self.state().load(Ordering::Acquire);
277 let writer_active = (s & WRITER_BIT) != 0;
278 let readers = s & READERS_MASK;
279 if !writer_active && readers == 0 {
280 let new = (s & READERS_MASK) | WRITER_BIT
282 | ((((s & WAITING_MASK) >> WAITING_SHIFT) - 1) << WAITING_SHIFT);
283 if self.state().compare_exchange(
284 s, new, Ordering::AcqRel, Ordering::Acquire,
285 ).is_ok() {
286 self.ring_sidecar.push_op(
287 crate::sidecar_ops::rw_lock::OP_WRITE,
288 if spins > 0 { 1 } else { 0 }, );
290 return WriteGuard { lock: self };
291 }
292 }
293 spins += 1;
294 if spins < 32 {
295 std::hint::spin_loop();
296 } else if spins < 256 {
297 std::thread::yield_now();
298 } else {
299 std::thread::sleep(std::time::Duration::from_micros(50));
300 }
301 }
302 }
303
304 pub fn reader_count(&self) -> u32 {
306 (self.state().load(Ordering::Acquire) & READERS_MASK) as u32
307 }
308
309 pub fn has_writer(&self) -> bool {
311 (self.state().load(Ordering::Acquire) & WRITER_BIT) != 0
312 }
313
314 pub fn waiting_writers(&self) -> u32 {
316 ((self.state().load(Ordering::Acquire) & WAITING_MASK) >> WAITING_SHIFT) as u32
317 }
318
319 fn release_read(&self) {
325 loop {
326 let s = self.state().load(Ordering::Acquire);
327 let readers = s & READERS_MASK;
328 debug_assert!(
329 readers > 0,
330 "SharedRWLock::release_read called when reader count is 0 - \
331 indicates a protocol violation (double-release or release \
332 without acquire). The lock counter will not be decremented.",
333 );
334 if readers == 0 { return; }
335 let new = (s & !READERS_MASK) | (readers - 1);
336 if self.state().compare_exchange(
337 s, new, Ordering::AcqRel, Ordering::Acquire,
338 ).is_ok() {
339 return;
340 }
341 }
342 }
343
344 fn release_write(&self) {
348 let prev = self.state().fetch_and(!WRITER_BIT, Ordering::AcqRel);
349 debug_assert!(
350 (prev & WRITER_BIT) != 0,
351 "SharedRWLock::release_write called when no writer holds the lock - \
352 indicates a protocol violation (double-release or release without \
353 acquire).",
354 );
355 }
356
357 pub fn release_read_for_blocking(&self) { self.release_read(); }
363
364 pub fn release_write_for_blocking(&self) { self.release_write(); }
367
368 pub fn flush(&self) -> Result<(), RWLockError> {
369 self.mmap.flush()?;
370 Ok(())
371 }
372 pub fn flush_async(&self) -> Result<(), RWLockError> {
373 self.mmap.flush_async()?;
374 Ok(())
375 }
376}
377
378pub struct ReadGuard<'a> { lock: &'a SharedRWLock }
379impl Drop for ReadGuard<'_> {
380 fn drop(&mut self) { self.lock.release_read(); }
381}
382
383pub struct WriteGuard<'a> { lock: &'a SharedRWLock }
384impl Drop for WriteGuard<'_> {
385 fn drop(&mut self) { self.lock.release_write(); }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use std::sync::atomic::{AtomicU32, Ordering as O};
392 use std::sync::Arc;
393 use std::thread;
394
395 fn tmp(name: &str) -> std::path::PathBuf {
396 let mut p = std::env::temp_dir();
397 let pid = std::process::id();
398 p.push(format!("subetha-rwlock-{name}-{pid}.bin"));
399 p
400 }
401
402 #[test]
409 fn create_or_open_racing_callers_do_not_clear_a_held_writer() {
410 let p = tmp("race");
411 std::fs::remove_file(&p).ok();
412
413 let holder = SharedRWLock::create_or_open(&p).unwrap();
414 let guard = holder.try_write_lock().expect("uncontended write lock");
415 assert!(holder.has_writer());
416
417 let path = Arc::new(p.clone());
419 let cleared = Arc::new(AtomicU32::new(0));
420 let mut hs = Vec::new();
421 for _ in 0..8 {
422 let path = Arc::clone(&path);
423 let cleared = Arc::clone(&cleared);
424 hs.push(thread::spawn(move || {
425 let l = SharedRWLock::create_or_open(&*path).expect("open existing");
426 if !l.has_writer() {
427 cleared.fetch_add(1, O::Relaxed);
428 }
429 }));
430 }
431 for h in hs {
432 h.join().unwrap();
433 }
434
435 assert_eq!(
436 cleared.load(O::Relaxed),
437 0,
438 "a concurrent create_or_open zeroed a writer flag another holder owned",
439 );
440 assert!(holder.has_writer(), "the holder lost its own write lock");
441 drop(guard);
442 std::fs::remove_file(&p).ok();
443 }
444
445 #[test]
446 fn create_initial_state_is_idle() {
447 let p = tmp("init");
448 let l = SharedRWLock::create(&p).unwrap();
449 assert_eq!(l.reader_count(), 0);
450 assert!(!l.has_writer());
451 assert_eq!(l.waiting_writers(), 0);
452 std::fs::remove_file(&p).ok();
453 }
454
455 #[test]
456 fn try_read_succeeds_when_idle() {
457 let p = tmp("try-read");
458 let l = SharedRWLock::create(&p).unwrap();
459 let g = l.try_read_lock().unwrap();
460 assert_eq!(l.reader_count(), 1);
461 drop(g);
462 assert_eq!(l.reader_count(), 0);
463 std::fs::remove_file(&p).ok();
464 }
465
466 #[test]
467 fn try_write_succeeds_when_idle() {
468 let p = tmp("try-write");
469 let l = SharedRWLock::create(&p).unwrap();
470 let g = l.try_write_lock().unwrap();
471 assert!(l.has_writer());
472 drop(g);
473 assert!(!l.has_writer());
474 std::fs::remove_file(&p).ok();
475 }
476
477 #[test]
478 fn multiple_readers_coexist() {
479 let p = tmp("multi-read");
480 let l = SharedRWLock::create(&p).unwrap();
481 let g1 = l.try_read_lock().unwrap();
482 let g2 = l.try_read_lock().unwrap();
483 let g3 = l.try_read_lock().unwrap();
484 assert_eq!(l.reader_count(), 3);
485 drop(g1); drop(g2); drop(g3);
486 assert_eq!(l.reader_count(), 0);
487 std::fs::remove_file(&p).ok();
488 }
489
490 #[test]
491 fn writer_excludes_readers() {
492 let p = tmp("w-excl-r");
493 let l = SharedRWLock::create(&p).unwrap();
494 let _w = l.try_write_lock().unwrap();
495 assert_eq!(l.try_read_lock().err(), Some(RWLockError::WouldBlock));
496 std::fs::remove_file(&p).ok();
497 }
498
499 #[test]
500 fn reader_excludes_writer() {
501 let p = tmp("r-excl-w");
502 let l = SharedRWLock::create(&p).unwrap();
503 let _r = l.try_read_lock().unwrap();
504 assert_eq!(l.try_write_lock().err(), Some(RWLockError::WouldBlock));
505 std::fs::remove_file(&p).ok();
506 }
507
508 #[test]
509 fn writer_excludes_writer() {
510 let p = tmp("w-excl-w");
511 let l = SharedRWLock::create(&p).unwrap();
512 let _w = l.try_write_lock().unwrap();
513 assert_eq!(l.try_write_lock().err(), Some(RWLockError::WouldBlock));
514 std::fs::remove_file(&p).ok();
515 }
516
517 #[test]
518 fn write_lock_blocks_until_readers_drop() {
519 let p = tmp("w-blocks");
525 let l = Arc::new(SharedRWLock::create(&p).unwrap());
526 let l_reader = l.clone();
527 let reader_done = Arc::new(AtomicU32::new(0));
528 let reader_done_clone = reader_done.clone();
529 let reader = thread::spawn(move || {
530 let _g = l_reader.read_lock();
531 std::thread::sleep(std::time::Duration::from_millis(30));
532 reader_done_clone.store(1, O::Release);
533 });
535 let acquire_deadline = std::time::Instant::now()
538 + std::time::Duration::from_secs(5);
539 while l.reader_count() != 1
540 && std::time::Instant::now() < acquire_deadline
541 {
542 std::thread::yield_now();
543 }
544 assert_eq!(l.reader_count(), 1);
545
546 let l_writer = l.clone();
547 let writer_started = std::time::Instant::now();
548 let writer = thread::spawn(move || {
549 let _g = l_writer.write_lock();
550 writer_started.elapsed()
551 });
552
553 let elapsed = writer.join().unwrap();
554 reader.join().unwrap();
555 assert!(
558 elapsed >= std::time::Duration::from_millis(15),
559 "writer should have blocked for the reader's hold time, got {elapsed:?}",
560 );
561 assert_eq!(reader_done.load(O::Acquire), 1);
562 std::fs::remove_file(&p).ok();
563 }
564
565 #[test]
566 fn writer_priority_blocks_new_readers() {
567 let p = tmp("w-priority");
570 let l = SharedRWLock::create(&p).unwrap();
571 l.state().fetch_add(1u64 << WAITING_SHIFT, Ordering::AcqRel);
574 assert_eq!(l.try_read_lock().err(), Some(RWLockError::WouldBlock));
575 l.state().fetch_sub(1u64 << WAITING_SHIFT, Ordering::AcqRel);
577 std::fs::remove_file(&p).ok();
578 }
579
580 #[test]
581 fn concurrent_readers_all_succeed() {
582 let p = tmp("concurrent-r");
583 let l = Arc::new(SharedRWLock::create(&p).unwrap());
584 let n = 8;
585 let count = Arc::new(AtomicU32::new(0));
586 let mut handles = vec![];
587 for _ in 0..n {
588 let l = l.clone();
589 let count = count.clone();
590 handles.push(thread::spawn(move || {
591 let _g = l.read_lock();
592 count.fetch_add(1, O::AcqRel);
593 std::thread::sleep(std::time::Duration::from_millis(5));
594 }));
595 }
596 for h in handles { h.join().unwrap(); }
597 assert_eq!(count.load(O::Acquire), n);
598 std::fs::remove_file(&p).ok();
599 }
600
601 #[test]
602 fn cross_handle_visibility() {
603 let p = tmp("cross-handle");
604 let w = SharedRWLock::create(&p).unwrap();
605 let r = SharedRWLock::open(&p).unwrap();
606 let _g = w.try_read_lock().unwrap();
607 assert_eq!(r.reader_count(), 1);
609 assert_eq!(r.try_write_lock().err(), Some(RWLockError::WouldBlock));
610 std::fs::remove_file(&p).ok();
611 }
612
613 #[test]
614 fn writer_then_reader_serialized() {
615 let p = tmp("w-then-r");
616 let l = SharedRWLock::create(&p).unwrap();
617 {
618 let _w = l.try_write_lock().unwrap();
619 }
620 let _r = l.try_read_lock().unwrap();
622 std::fs::remove_file(&p).ok();
623 }
624}