noxu_sync/lib.rs
1// Copyright (C) 2024-2025 Greg Burd. Licensed under either of the
2// Apache License, Version 2.0 or the MIT license, at your option.
3// See LICENSE-APACHE and LICENSE-MIT at the root of this repository.
4// SPDX-License-Identifier: Apache-2.0 OR MIT
5
6//! > **Internal component of the [`noxu`](https://crates.io/crates/noxu) database.**
7//! >
8//! > This crate is published only so the `noxu` umbrella crate can depend on it.
9//! > Use `noxu` (`noxu = "3"`) in applications; depend on this crate directly only
10//! > if you are extending the engine internals. Its API may change without a major
11//! > version bump.
12//!
13//! Noxu DB synchronization primitives.
14//!
15//! Futex-based `Mutex<T>`, `RwLock<T>`, and `Condvar` that replace
16//! `parking_lot` throughout the Noxu codebase.
17//!
18//! ## Extra capabilities vs parking_lot
19//!
20//! | Method | Description |
21//! |--------|-------------|
22//! | `Mutex::get_n_waiters()` | Count of threads blocked waiting for the mutex |
23//! | `Mutex::get_owner()` | Thread ID hash of the current owner |
24//! | `RwLock::get_n_waiters()` | Count of threads blocked waiting for the rwlock |
25//! | `RwLock::is_locked_exclusive()` | Returns true when a write lock is held |
26//! | `RwLock::reader_count()` | Number of active shared-lock holders (global) |
27//!
28//! ## Drop-in compatibility
29//!
30//! The public types are designed to be drop-in replacements for the
31//! corresponding `parking_lot` types:
32//!
33//! ```text
34//! parking_lot::Mutex<T> → noxu_sync::Mutex<T>
35//! parking_lot::RwLock<T> → noxu_sync::RwLock<T>
36//! parking_lot::Condvar → noxu_sync::Condvar
37//! parking_lot::RawMutex → noxu_sync::RawMutex
38//! parking_lot::lock_api::… → noxu_sync::lock_api::…
39//! parking_lot::MutexGuard<'_,T> → noxu_sync::MutexGuard<'_,T>
40//! parking_lot::WaitTimeoutResult → noxu_sync::WaitTimeoutResult
41//! ```
42
43pub mod condvar;
44pub mod futex;
45pub mod raw_mutex;
46pub mod raw_rwlock;
47
48pub use condvar::{Condvar, WaitTimeoutResult};
49pub use raw_mutex::NoxuRawMutex;
50pub use raw_rwlock::NoxuRawRwLock;
51
52/// Re-export `lock_api` so callers can do `use noxu_sync::lock_api::RawMutex`.
53pub use lock_api;
54
55// ---------------------------------------------------------------------------
56// Mutex
57// ---------------------------------------------------------------------------
58
59/// Mutual exclusion primitive backed by a futex.
60///
61/// Drop-in replacement for `parking_lot::Mutex<T>`.
62pub type Mutex<T> = lock_api::Mutex<NoxuRawMutex, T>;
63
64/// RAII guard returned by `Mutex::lock`.
65pub type MutexGuard<'a, T> = lock_api::MutexGuard<'a, NoxuRawMutex, T>;
66
67/// The raw mutex type for direct embed (e.g., `log_buffer.rs`).
68///
69/// Provides `RawMutex::INIT` for const initialisation and implements
70/// `lock_api::RawMutex` for `.lock()` / `unsafe .unlock()`.
71pub type RawMutex = NoxuRawMutex;
72
73// ---------------------------------------------------------------------------
74// RwLock
75// ---------------------------------------------------------------------------
76
77/// RAII guard returned by `RwLock::read`.
78pub type RwLockReadGuard<'a, T> =
79 lock_api::RwLockReadGuard<'a, NoxuRawRwLock, T>;
80
81/// RAII guard returned by `RwLock::write`.
82pub type RwLockWriteGuard<'a, T> =
83 lock_api::RwLockWriteGuard<'a, NoxuRawRwLock, T>;
84
85/// Reader-writer lock backed by a futex.
86///
87/// Drop-in replacement for `parking_lot::RwLock<T>`.
88/// Non-fair: new readers are not blocked by waiting writers.
89///
90/// Additional methods beyond parking_lot:
91/// - `is_locked_exclusive()` — true when a write lock is held
92/// - `get_n_waiters()` — number of threads waiting (read + write)
93/// - `reader_count()` — number of active readers
94pub struct RwLock<T>(lock_api::RwLock<NoxuRawRwLock, T>);
95
96impl<T> RwLock<T> {
97 /// Creates a new `RwLock` wrapping `val`.
98 #[inline]
99 pub fn new(val: T) -> Self {
100 RwLock(lock_api::RwLock::new(val))
101 }
102
103 /// Acquires a shared (read) lock, blocking until available.
104 #[inline]
105 pub fn read(&self) -> RwLockReadGuard<'_, T> {
106 self.0.read()
107 }
108
109 /// Acquires an exclusive (write) lock, blocking until available.
110 #[inline]
111 pub fn write(&self) -> RwLockWriteGuard<'_, T> {
112 self.0.write()
113 }
114
115 /// Tries to acquire a shared lock without blocking.
116 #[inline]
117 pub fn try_read(&self) -> Option<RwLockReadGuard<'_, T>> {
118 self.0.try_read()
119 }
120
121 /// Tries to acquire an exclusive lock without blocking.
122 #[inline]
123 pub fn try_write(&self) -> Option<RwLockWriteGuard<'_, T>> {
124 self.0.try_write()
125 }
126
127 /// Tries to acquire a shared lock within the given `timeout`.
128 #[inline]
129 pub fn try_read_for(
130 &self,
131 timeout: std::time::Duration,
132 ) -> Option<RwLockReadGuard<'_, T>> {
133 self.0.try_read_for(timeout)
134 }
135
136 /// Tries to acquire an exclusive lock within the given `timeout`.
137 #[inline]
138 pub fn try_write_for(
139 &self,
140 timeout: std::time::Duration,
141 ) -> Option<RwLockWriteGuard<'_, T>> {
142 self.0.try_write_for(timeout)
143 }
144
145 /// Returns `true` if the lock is held by any reader or by the exclusive writer.
146 #[inline]
147 pub fn is_locked(&self) -> bool {
148 self.0.is_locked()
149 }
150
151 /// Returns `true` if the write (exclusive) lock is currently held.
152 #[inline]
153 pub fn is_locked_exclusive(&self) -> bool {
154 // SAFETY: raw() is safe to call; we only read atomic state.
155 unsafe { self.0.raw().is_write_locked() }
156 }
157
158 /// Returns the total number of threads waiting to acquire this lock.
159 #[inline]
160 pub fn get_n_waiters(&self) -> usize {
161 unsafe { self.0.raw().get_n_waiters() }
162 }
163
164 /// Returns the number of active readers.
165 #[inline]
166 pub fn reader_count(&self) -> u32 {
167 unsafe { self.0.raw().reader_count() }
168 }
169
170 /// Returns a reference to the raw `NoxuRawRwLock`.
171 #[inline]
172 pub fn raw(&self) -> &NoxuRawRwLock {
173 unsafe { self.0.raw() }
174 }
175}
176
177// ---------------------------------------------------------------------------
178// Tests
179// ---------------------------------------------------------------------------
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use std::sync::Arc;
185 use std::time::Duration;
186
187 // --- Mutex ---
188
189 #[test]
190 fn mutex_basic() {
191 let m = Mutex::new(0i32);
192 *m.lock() = 42;
193 assert_eq!(*m.lock(), 42);
194 }
195
196 #[test]
197 fn mutex_try_lock() {
198 let m = Arc::new(Mutex::new(()));
199 let g = m.lock();
200 let m2 = m.clone();
201 let failed =
202 std::thread::spawn(move || m2.try_lock().is_none()).join().unwrap();
203 assert!(failed);
204 drop(g);
205 assert!(m.try_lock().is_some());
206 }
207
208 #[test]
209 fn mutex_try_lock_for_timeout() {
210 let m = Arc::new(Mutex::new(()));
211 let g = m.lock();
212 let m2 = m.clone();
213 let timed_out = std::thread::spawn(move || {
214 m2.try_lock_for(Duration::from_millis(30)).is_none()
215 })
216 .join()
217 .unwrap();
218 assert!(timed_out);
219 drop(g);
220 }
221
222 #[test]
223 fn mutex_get_n_waiters() {
224 let m = Arc::new(Mutex::new(()));
225 let _g = m.lock();
226 let m2 = m.clone();
227 let barrier = Arc::new(std::sync::Barrier::new(2));
228 let b2 = barrier.clone();
229 let handle = std::thread::spawn(move || {
230 b2.wait();
231 let _g2 = m2.lock();
232 });
233 barrier.wait();
234 std::thread::sleep(Duration::from_millis(10));
235 // The raw() accessor exposes get_n_waiters.
236 assert!(unsafe { m.raw().get_n_waiters() } >= 1 || m.is_locked());
237 drop(_g);
238 handle.join().unwrap();
239 }
240
241 #[test]
242 fn mutex_force_unlock() {
243 let m = Mutex::new(());
244 let _g = m.lock();
245 unsafe { m.force_unlock() };
246 // Should be acquirable now.
247 assert!(m.try_lock().is_some());
248 }
249
250 // --- RwLock ---
251
252 #[test]
253 fn rwlock_basic_read_write() {
254 let rw = RwLock::new(0i32);
255 *rw.write() = 99;
256 assert_eq!(*rw.read(), 99);
257 }
258
259 #[test]
260 fn rwlock_multiple_readers() {
261 let rw = Arc::new(RwLock::new(42i32));
262 let rw2 = rw.clone();
263 let g1 = rw.read();
264 let handle = std::thread::spawn(move || {
265 let g2 = rw2.read();
266 *g2
267 });
268 assert_eq!(*g1, 42);
269 assert_eq!(handle.join().unwrap(), 42);
270 }
271
272 #[test]
273 fn rwlock_exclusive_blocks_readers() {
274 let rw = Arc::new(RwLock::new(()));
275 let _wg = rw.write();
276 let rw2 = rw.clone();
277 let failed = std::thread::spawn(move || rw2.try_read().is_none())
278 .join()
279 .unwrap();
280 assert!(failed);
281 }
282
283 #[test]
284 fn rwlock_is_locked_exclusive() {
285 let rw = RwLock::new(());
286 assert!(!rw.is_locked_exclusive());
287 let _wg = rw.write();
288 assert!(rw.is_locked_exclusive());
289 }
290
291 #[test]
292 fn rwlock_try_write_for_timeout() {
293 let rw = Arc::new(RwLock::new(()));
294 let _wg = rw.write();
295 let rw2 = rw.clone();
296 let timed_out = std::thread::spawn(move || {
297 rw2.try_write_for(Duration::from_millis(30)).is_none()
298 })
299 .join()
300 .unwrap();
301 assert!(timed_out);
302 }
303
304 #[test]
305 fn rwlock_try_read_for_timeout() {
306 let rw = Arc::new(RwLock::new(()));
307 let _wg = rw.write();
308 let rw2 = rw.clone();
309 let timed_out = std::thread::spawn(move || {
310 rw2.try_read_for(Duration::from_millis(30)).is_none()
311 })
312 .join()
313 .unwrap();
314 assert!(timed_out);
315 }
316
317 // --- RawMutex (for log_buffer style usage) ---
318
319 #[test]
320 fn raw_mutex_const_init_and_lock_unlock() {
321 use lock_api::RawMutex as RawMutexTrait;
322 let raw = NoxuRawMutex::INIT;
323 assert!(!raw.is_locked());
324 raw.lock();
325 assert!(raw.is_locked());
326 unsafe { raw.unlock() };
327 assert!(!raw.is_locked());
328 }
329}