From ebdb6a18fe2892ccf110c9650dc4c889d84f93c1 Mon Sep 17 00:00:00 2001 From: Bert Peters Date: Sat, 1 Mar 2025 12:39:55 +0100 Subject: [PATCH 1/4] Restructure stdsync module --- src/stdsync.rs | 684 +---------------------------------------- src/stdsync/tracing.rs | 681 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 682 insertions(+), 683 deletions(-) create mode 100644 src/stdsync/tracing.rs diff --git a/src/stdsync.rs b/src/stdsync.rs index 76a3f69..65a1421 100644 --- a/src/stdsync.rs +++ b/src/stdsync.rs @@ -37,686 +37,4 @@ pub use tracing::LazyLock; pub use std::sync::LazyLock; /// Dependency tracing versions of [`std::sync`]. -pub mod tracing { - use std::fmt; - use std::ops::Deref; - use std::ops::DerefMut; - use std::sync; - use std::sync::LockResult; - use std::sync::OnceState; - use std::sync::PoisonError; - use std::sync::TryLockError; - use std::sync::TryLockResult; - use std::sync::WaitTimeoutResult; - use std::time::Duration; - - use crate::BorrowedMutex; - use crate::LazyMutexId; - - #[cfg(has_std__sync__LazyLock)] - pub use lazy_lock::LazyLock; - - #[cfg(has_std__sync__LazyLock)] - mod lazy_lock; - - /// Wrapper for [`std::sync::Mutex`]. - /// - /// Refer to the [crate-level][`crate`] documentation for the differences between this struct and - /// the one it wraps. - #[derive(Debug, Default)] - pub struct Mutex { - inner: sync::Mutex, - id: LazyMutexId, - } - - /// Wrapper for [`std::sync::MutexGuard`]. - /// - /// Refer to the [crate-level][`crate`] documentation for the differences between this struct and - /// the one it wraps. - #[derive(Debug)] - pub struct MutexGuard<'a, T> { - inner: sync::MutexGuard<'a, T>, - _mutex: BorrowedMutex<'a>, - } - - fn map_lockresult(result: LockResult, mapper: F) -> LockResult - where - F: FnOnce(I) -> T, - { - match result { - Ok(inner) => Ok(mapper(inner)), - Err(poisoned) => Err(PoisonError::new(mapper(poisoned.into_inner()))), - } - } - - fn map_trylockresult(result: TryLockResult, mapper: F) -> TryLockResult - where - F: FnOnce(I) -> T, - { - match result { - Ok(inner) => Ok(mapper(inner)), - Err(TryLockError::WouldBlock) => Err(TryLockError::WouldBlock), - Err(TryLockError::Poisoned(poisoned)) => { - Err(PoisonError::new(mapper(poisoned.into_inner())).into()) - } - } - } - - impl Mutex { - /// Create a new tracing mutex with the provided value. - pub const fn new(t: T) -> Self { - Self { - inner: sync::Mutex::new(t), - id: LazyMutexId::new(), - } - } - - /// Wrapper for [`std::sync::Mutex::lock`]. - /// - /// # Panics - /// - /// This method participates in lock dependency tracking. If acquiring this lock introduces a - /// dependency cycle, this method will panic. - #[track_caller] - pub fn lock(&self) -> LockResult> { - let mutex = self.id.get_borrowed(); - let result = self.inner.lock(); - - let mapper = |guard| MutexGuard { - _mutex: mutex, - inner: guard, - }; - - map_lockresult(result, mapper) - } - - /// Wrapper for [`std::sync::Mutex::try_lock`]. - /// - /// # Panics - /// - /// This method participates in lock dependency tracking. If acquiring this lock introduces a - /// dependency cycle, this method will panic. - #[track_caller] - pub fn try_lock(&self) -> TryLockResult> { - let mutex = self.id.get_borrowed(); - let result = self.inner.try_lock(); - - let mapper = |guard| MutexGuard { - _mutex: mutex, - inner: guard, - }; - - map_trylockresult(result, mapper) - } - - /// Wrapper for [`std::sync::Mutex::is_poisoned`]. - pub fn is_poisoned(&self) -> bool { - self.inner.is_poisoned() - } - - /// Return a mutable reference to the underlying data. - /// - /// This method does not block as the locking is handled compile-time by the type system. - pub fn get_mut(&mut self) -> LockResult<&mut T> { - self.inner.get_mut() - } - - /// Unwrap the mutex and return its inner value. - pub fn into_inner(self) -> LockResult { - self.inner.into_inner() - } - } - - impl From for Mutex { - fn from(t: T) -> Self { - Self::new(t) - } - } - - impl Deref for MutexGuard<'_, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } - } - - impl DerefMut for MutexGuard<'_, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } - } - - impl fmt::Display for MutexGuard<'_, T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner.fmt(f) - } - } - - /// Wrapper around [`std::sync::Condvar`]. - /// - /// Allows `TracingMutexGuard` to be used with a `Condvar`. Unlike other structs in this module, - /// this wrapper does not add any additional dependency tracking or other overhead on top of the - /// primitive it wraps. All dependency tracking happens through the mutexes itself. - /// - /// # Panics - /// - /// This struct does not add any panics over the base implementation of `Condvar`, but panics due to - /// dependency tracking may poison associated mutexes. - /// - /// # Examples - /// - /// ``` - /// use std::sync::Arc; - /// use std::thread; - /// - /// use tracing_mutex::stdsync::tracing::{Condvar, Mutex}; - /// - /// let pair = Arc::new((Mutex::new(false), Condvar::new())); - /// let pair2 = Arc::clone(&pair); - /// - /// // Spawn a thread that will unlock the condvar - /// thread::spawn(move || { - /// let (lock, condvar) = &*pair2; - /// *lock.lock().unwrap() = true; - /// condvar.notify_one(); - /// }); - /// - /// // Wait until the thread unlocks the condvar - /// let (lock, condvar) = &*pair; - /// let guard = lock.lock().unwrap(); - /// let guard = condvar.wait_while(guard, |started| !*started).unwrap(); - /// - /// // Guard should read true now - /// assert!(*guard); - /// ``` - #[derive(Debug, Default)] - pub struct Condvar(sync::Condvar); - - impl Condvar { - /// Creates a new condition variable which is ready to be waited on and notified. - pub const fn new() -> Self { - Self(sync::Condvar::new()) - } - - /// Wrapper for [`std::sync::Condvar::wait`]. - pub fn wait<'a, T>(&self, guard: MutexGuard<'a, T>) -> LockResult> { - let MutexGuard { _mutex, inner } = guard; - - map_lockresult(self.0.wait(inner), |inner| MutexGuard { _mutex, inner }) - } - - /// Wrapper for [`std::sync::Condvar::wait_while`]. - pub fn wait_while<'a, T, F>( - &self, - guard: MutexGuard<'a, T>, - condition: F, - ) -> LockResult> - where - F: FnMut(&mut T) -> bool, - { - let MutexGuard { _mutex, inner } = guard; - - map_lockresult(self.0.wait_while(inner, condition), |inner| MutexGuard { - _mutex, - inner, - }) - } - - /// Wrapper for [`std::sync::Condvar::wait_timeout`]. - pub fn wait_timeout<'a, T>( - &self, - guard: MutexGuard<'a, T>, - dur: Duration, - ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)> { - let MutexGuard { _mutex, inner } = guard; - - map_lockresult(self.0.wait_timeout(inner, dur), |(inner, result)| { - (MutexGuard { _mutex, inner }, result) - }) - } - - /// Wrapper for [`std::sync::Condvar::wait_timeout_while`]. - pub fn wait_timeout_while<'a, T, F>( - &self, - guard: MutexGuard<'a, T>, - dur: Duration, - condition: F, - ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)> - where - F: FnMut(&mut T) -> bool, - { - let MutexGuard { _mutex, inner } = guard; - - map_lockresult( - self.0.wait_timeout_while(inner, dur, condition), - |(inner, result)| (MutexGuard { _mutex, inner }, result), - ) - } - - /// Wrapper for [`std::sync::Condvar::notify_one`]. - pub fn notify_one(&self) { - self.0.notify_one(); - } - - /// Wrapper for [`std::sync::Condvar::notify_all`]. - pub fn notify_all(&self) { - self.0.notify_all(); - } - } - - /// Wrapper for [`std::sync::RwLock`]. - #[derive(Debug, Default)] - pub struct RwLock { - inner: sync::RwLock, - id: LazyMutexId, - } - - /// Hybrid wrapper for both [`std::sync::RwLockReadGuard`] and [`std::sync::RwLockWriteGuard`]. - /// - /// Please refer to [`RwLockReadGuard`] and [`RwLockWriteGuard`] for usable types. - #[derive(Debug)] - pub struct TracingRwLockGuard<'a, L> { - inner: L, - _mutex: BorrowedMutex<'a>, - } - - /// Wrapper around [`std::sync::RwLockReadGuard`]. - pub type RwLockReadGuard<'a, T> = TracingRwLockGuard<'a, sync::RwLockReadGuard<'a, T>>; - /// Wrapper around [`std::sync::RwLockWriteGuard`]. - pub type RwLockWriteGuard<'a, T> = TracingRwLockGuard<'a, sync::RwLockWriteGuard<'a, T>>; - - impl RwLock { - pub const fn new(t: T) -> Self { - Self { - inner: sync::RwLock::new(t), - id: LazyMutexId::new(), - } - } - - /// Wrapper for [`std::sync::RwLock::read`]. - /// - /// # Panics - /// - /// This method participates in lock dependency tracking. If acquiring this lock introduces a - /// dependency cycle, this method will panic. - #[track_caller] - pub fn read(&self) -> LockResult> { - let mutex = self.id.get_borrowed(); - let result = self.inner.read(); - - map_lockresult(result, |inner| TracingRwLockGuard { - inner, - _mutex: mutex, - }) - } - - /// Wrapper for [`std::sync::RwLock::write`]. - /// - /// # Panics - /// - /// This method participates in lock dependency tracking. If acquiring this lock introduces a - /// dependency cycle, this method will panic. - #[track_caller] - pub fn write(&self) -> LockResult> { - let mutex = self.id.get_borrowed(); - let result = self.inner.write(); - - map_lockresult(result, |inner| TracingRwLockGuard { - inner, - _mutex: mutex, - }) - } - - /// Wrapper for [`std::sync::RwLock::try_read`]. - /// - /// # Panics - /// - /// This method participates in lock dependency tracking. If acquiring this lock introduces a - /// dependency cycle, this method will panic. - #[track_caller] - pub fn try_read(&self) -> TryLockResult> { - let mutex = self.id.get_borrowed(); - let result = self.inner.try_read(); - - map_trylockresult(result, |inner| TracingRwLockGuard { - inner, - _mutex: mutex, - }) - } - - /// Wrapper for [`std::sync::RwLock::try_write`]. - /// - /// # Panics - /// - /// This method participates in lock dependency tracking. If acquiring this lock introduces a - /// dependency cycle, this method will panic. - #[track_caller] - pub fn try_write(&self) -> TryLockResult> { - let mutex = self.id.get_borrowed(); - let result = self.inner.try_write(); - - map_trylockresult(result, |inner| TracingRwLockGuard { - inner, - _mutex: mutex, - }) - } - - /// Return a mutable reference to the underlying data. - /// - /// This method does not block as the locking is handled compile-time by the type system. - pub fn get_mut(&mut self) -> LockResult<&mut T> { - self.inner.get_mut() - } - - /// Unwrap the mutex and return its inner value. - pub fn into_inner(self) -> LockResult { - self.inner.into_inner() - } - } - - impl From for RwLock { - fn from(t: T) -> Self { - Self::new(t) - } - } - - impl Deref for TracingRwLockGuard<'_, L> - where - L: Deref, - { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.inner.deref() - } - } - - impl DerefMut for TracingRwLockGuard<'_, L> - where - L: Deref + DerefMut, - { - fn deref_mut(&mut self) -> &mut Self::Target { - self.inner.deref_mut() - } - } - - /// Wrapper around [`std::sync::Once`]. - /// - /// Refer to the [crate-level][`crate`] documentaiton for the differences between this struct - /// and the one it wraps. - #[derive(Debug)] - pub struct Once { - inner: sync::Once, - mutex_id: LazyMutexId, - } - - // New without default is intentional, `std::sync::Once` doesn't implement it either - #[allow(clippy::new_without_default)] - impl Once { - /// Create a new `Once` value. - pub const fn new() -> Self { - Self { - inner: sync::Once::new(), - mutex_id: LazyMutexId::new(), - } - } - - /// Wrapper for [`std::sync::Once::call_once`]. - /// - /// # Panics - /// - /// In addition to the panics that `Once` can cause, this method will panic if calling it - /// introduces a cycle in the lock dependency graph. - pub fn call_once(&self, f: F) - where - F: FnOnce(), - { - self.mutex_id.with_held(|| self.inner.call_once(f)) - } - - /// Performs the same operation as [`call_once`][Once::call_once] except it ignores - /// poisoning. - /// - /// # Panics - /// - /// This method participates in lock dependency tracking. If acquiring this lock introduces a - /// dependency cycle, this method will panic. - pub fn call_once_force(&self, f: F) - where - F: FnOnce(&OnceState), - { - self.mutex_id.with_held(|| self.inner.call_once_force(f)) - } - - /// Returns true if some `call_once` has completed successfully. - pub fn is_completed(&self) -> bool { - self.inner.is_completed() - } - } - - /// Wrapper for [`std::sync::OnceLock`] - /// - /// The exact locking behaviour of [`std::sync::OnceLock`] is currently undefined, but may - /// deadlock in the event of reentrant initialization attempts. This wrapper participates in - /// cycle detection as normal and will therefore panic in the event of reentrancy. - /// - /// Most of this primitive's methods do not involve locking and as such are simply passed - /// through to the inner implementation. - /// - /// # Examples - /// - /// ``` - /// use tracing_mutex::stdsync::tracing::OnceLock; - /// - /// static LOCK: OnceLock = OnceLock::new(); - /// assert!(LOCK.get().is_none()); - /// - /// std::thread::spawn(|| { - /// let value: &i32 = LOCK.get_or_init(|| 42); - /// assert_eq!(value, &42); - /// }).join().unwrap(); - /// - /// let value: Option<&i32> = LOCK.get(); - /// assert_eq!(value, Some(&42)); - /// ``` - #[derive(Debug)] - pub struct OnceLock { - id: LazyMutexId, - inner: sync::OnceLock, - } - - // N.B. this impl inlines everything that directly calls the inner implementation as there - // should be 0 overhead to doing so. - impl OnceLock { - /// Creates a new empty cell - pub const fn new() -> Self { - Self { - id: LazyMutexId::new(), - inner: sync::OnceLock::new(), - } - } - - /// Gets a reference to the underlying value. - /// - /// This method does not attempt to lock and therefore does not participate in cycle - /// detection. - #[inline] - pub fn get(&self) -> Option<&T> { - self.inner.get() - } - - /// Gets a mutable reference to the underlying value. - /// - /// This method does not attempt to lock and therefore does not participate in cycle - /// detection. - #[inline] - pub fn get_mut(&mut self) -> Option<&mut T> { - self.inner.get_mut() - } - - /// Sets the contents of this cell to the underlying value - /// - /// As this method may block until initialization is complete, it participates in cycle - /// detection. - pub fn set(&self, value: T) -> Result<(), T> { - self.id.with_held(|| self.inner.set(value)) - } - - /// Gets the contents of the cell, initializing it with `f` if the cell was empty. - /// - /// This method participates in cycle detection. Reentrancy is considered a cycle. - pub fn get_or_init(&self, f: F) -> &T - where - F: FnOnce() -> T, - { - self.id.with_held(|| self.inner.get_or_init(f)) - } - - /// Takes the value out of this `OnceLock`, moving it back to an uninitialized state. - /// - /// This method does not attempt to lock and therefore does not participate in cycle - /// detection. - #[inline] - pub fn take(&mut self) -> Option { - self.inner.take() - } - - /// Consumes the `OnceLock`, returning the wrapped value. Returns None if the cell was - /// empty. - /// - /// This method does not attempt to lock and therefore does not participate in cycle - /// detection. - #[inline] - pub fn into_inner(mut self) -> Option { - self.take() - } - } - - impl Default for OnceLock { - #[inline] - fn default() -> Self { - Self::new() - } - } - - impl PartialEq for OnceLock { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.inner == other.inner - } - } - - impl Eq for OnceLock {} - - impl Clone for OnceLock { - fn clone(&self) -> Self { - Self { - id: LazyMutexId::new(), - inner: self.inner.clone(), - } - } - } - - impl From for OnceLock { - #[inline] - fn from(value: T) -> Self { - Self { - id: LazyMutexId::new(), - inner: sync::OnceLock::from(value), - } - } - } - - #[cfg(test)] - mod tests { - use std::sync::Arc; - use std::thread; - - use super::*; - - #[test] - fn test_mutex_usage() { - let mutex = Arc::new(Mutex::new(0)); - - assert_eq!(*mutex.lock().unwrap(), 0); - *mutex.lock().unwrap() = 1; - assert_eq!(*mutex.lock().unwrap(), 1); - - let mutex_clone = mutex.clone(); - - let _guard = mutex.lock().unwrap(); - - // Now try to cause a blocking exception in another thread - let handle = thread::spawn(move || { - let result = mutex_clone.try_lock().unwrap_err(); - - assert!(matches!(result, TryLockError::WouldBlock)); - }); - - handle.join().unwrap(); - } - - #[test] - fn test_rwlock_usage() { - let rwlock = Arc::new(RwLock::new(0)); - - assert_eq!(*rwlock.read().unwrap(), 0); - assert_eq!(*rwlock.write().unwrap(), 0); - *rwlock.write().unwrap() = 1; - assert_eq!(*rwlock.read().unwrap(), 1); - assert_eq!(*rwlock.write().unwrap(), 1); - - let rwlock_clone = rwlock.clone(); - - let _read_lock = rwlock.read().unwrap(); - - // Now try to cause a blocking exception in another thread - let handle = thread::spawn(move || { - let write_result = rwlock_clone.try_write().unwrap_err(); - - assert!(matches!(write_result, TryLockError::WouldBlock)); - - // Should be able to get a read lock just fine. - let _read_lock = rwlock_clone.read().unwrap(); - }); - - handle.join().unwrap(); - } - - #[test] - fn test_once_usage() { - let once = Arc::new(Once::new()); - let once_clone = once.clone(); - - assert!(!once.is_completed()); - - let handle = thread::spawn(move || { - assert!(!once_clone.is_completed()); - - once_clone.call_once(|| {}); - - assert!(once_clone.is_completed()); - }); - - handle.join().unwrap(); - - assert!(once.is_completed()); - } - - #[test] - #[should_panic(expected = "Found cycle in mutex dependency graph")] - fn test_detect_cycle() { - let a = Mutex::new(()); - let b = Mutex::new(()); - - let hold_a = a.lock().unwrap(); - let _ = b.lock(); - - drop(hold_a); - - let _hold_b = b.lock().unwrap(); - let _ = a.lock(); - } - } -} +pub mod tracing; diff --git a/src/stdsync/tracing.rs b/src/stdsync/tracing.rs new file mode 100644 index 0000000..afc6998 --- /dev/null +++ b/src/stdsync/tracing.rs @@ -0,0 +1,681 @@ +use std::fmt; +use std::ops::Deref; +use std::ops::DerefMut; +use std::sync; +use std::sync::LockResult; +use std::sync::OnceState; +use std::sync::PoisonError; +use std::sync::TryLockError; +use std::sync::TryLockResult; +use std::sync::WaitTimeoutResult; +use std::time::Duration; + +use crate::BorrowedMutex; +use crate::LazyMutexId; + +#[cfg(has_std__sync__LazyLock)] +pub use lazy_lock::LazyLock; + +#[cfg(has_std__sync__LazyLock)] +mod lazy_lock; + +/// Wrapper for [`std::sync::Mutex`]. +/// +/// Refer to the [crate-level][`crate`] documentation for the differences between this struct and +/// the one it wraps. +#[derive(Debug, Default)] +pub struct Mutex { + inner: sync::Mutex, + id: LazyMutexId, +} + +/// Wrapper for [`std::sync::MutexGuard`]. +/// +/// Refer to the [crate-level][`crate`] documentation for the differences between this struct and +/// the one it wraps. +#[derive(Debug)] +pub struct MutexGuard<'a, T> { + inner: sync::MutexGuard<'a, T>, + _mutex: BorrowedMutex<'a>, +} + +fn map_lockresult(result: LockResult, mapper: F) -> LockResult +where + F: FnOnce(I) -> T, +{ + match result { + Ok(inner) => Ok(mapper(inner)), + Err(poisoned) => Err(PoisonError::new(mapper(poisoned.into_inner()))), + } +} + +fn map_trylockresult(result: TryLockResult, mapper: F) -> TryLockResult +where + F: FnOnce(I) -> T, +{ + match result { + Ok(inner) => Ok(mapper(inner)), + Err(TryLockError::WouldBlock) => Err(TryLockError::WouldBlock), + Err(TryLockError::Poisoned(poisoned)) => { + Err(PoisonError::new(mapper(poisoned.into_inner())).into()) + } + } +} + +impl Mutex { + /// Create a new tracing mutex with the provided value. + pub const fn new(t: T) -> Self { + Self { + inner: sync::Mutex::new(t), + id: LazyMutexId::new(), + } + } + + /// Wrapper for [`std::sync::Mutex::lock`]. + /// + /// # Panics + /// + /// This method participates in lock dependency tracking. If acquiring this lock introduces a + /// dependency cycle, this method will panic. + #[track_caller] + pub fn lock(&self) -> LockResult> { + let mutex = self.id.get_borrowed(); + let result = self.inner.lock(); + + let mapper = |guard| MutexGuard { + _mutex: mutex, + inner: guard, + }; + + map_lockresult(result, mapper) + } + + /// Wrapper for [`std::sync::Mutex::try_lock`]. + /// + /// # Panics + /// + /// This method participates in lock dependency tracking. If acquiring this lock introduces a + /// dependency cycle, this method will panic. + #[track_caller] + pub fn try_lock(&self) -> TryLockResult> { + let mutex = self.id.get_borrowed(); + let result = self.inner.try_lock(); + + let mapper = |guard| MutexGuard { + _mutex: mutex, + inner: guard, + }; + + map_trylockresult(result, mapper) + } + + /// Wrapper for [`std::sync::Mutex::is_poisoned`]. + pub fn is_poisoned(&self) -> bool { + self.inner.is_poisoned() + } + + /// Return a mutable reference to the underlying data. + /// + /// This method does not block as the locking is handled compile-time by the type system. + pub fn get_mut(&mut self) -> LockResult<&mut T> { + self.inner.get_mut() + } + + /// Unwrap the mutex and return its inner value. + pub fn into_inner(self) -> LockResult { + self.inner.into_inner() + } +} + +impl From for Mutex { + fn from(t: T) -> Self { + Self::new(t) + } +} + +impl Deref for MutexGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for MutexGuard<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl fmt::Display for MutexGuard<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} + +/// Wrapper around [`std::sync::Condvar`]. +/// +/// Allows `TracingMutexGuard` to be used with a `Condvar`. Unlike other structs in this module, +/// this wrapper does not add any additional dependency tracking or other overhead on top of the +/// primitive it wraps. All dependency tracking happens through the mutexes itself. +/// +/// # Panics +/// +/// This struct does not add any panics over the base implementation of `Condvar`, but panics due to +/// dependency tracking may poison associated mutexes. +/// +/// # Examples +/// +/// ``` +/// use std::sync::Arc; +/// use std::thread; +/// +/// use tracing_mutex::stdsync::tracing::{Condvar, Mutex}; +/// +/// let pair = Arc::new((Mutex::new(false), Condvar::new())); +/// let pair2 = Arc::clone(&pair); +/// +/// // Spawn a thread that will unlock the condvar +/// thread::spawn(move || { +/// let (lock, condvar) = &*pair2; +/// *lock.lock().unwrap() = true; +/// condvar.notify_one(); +/// }); +/// +/// // Wait until the thread unlocks the condvar +/// let (lock, condvar) = &*pair; +/// let guard = lock.lock().unwrap(); +/// let guard = condvar.wait_while(guard, |started| !*started).unwrap(); +/// +/// // Guard should read true now +/// assert!(*guard); +/// ``` +#[derive(Debug, Default)] +pub struct Condvar(sync::Condvar); + +impl Condvar { + /// Creates a new condition variable which is ready to be waited on and notified. + pub const fn new() -> Self { + Self(sync::Condvar::new()) + } + + /// Wrapper for [`std::sync::Condvar::wait`]. + pub fn wait<'a, T>(&self, guard: MutexGuard<'a, T>) -> LockResult> { + let MutexGuard { _mutex, inner } = guard; + + map_lockresult(self.0.wait(inner), |inner| MutexGuard { _mutex, inner }) + } + + /// Wrapper for [`std::sync::Condvar::wait_while`]. + pub fn wait_while<'a, T, F>( + &self, + guard: MutexGuard<'a, T>, + condition: F, + ) -> LockResult> + where + F: FnMut(&mut T) -> bool, + { + let MutexGuard { _mutex, inner } = guard; + + map_lockresult(self.0.wait_while(inner, condition), |inner| MutexGuard { + _mutex, + inner, + }) + } + + /// Wrapper for [`std::sync::Condvar::wait_timeout`]. + pub fn wait_timeout<'a, T>( + &self, + guard: MutexGuard<'a, T>, + dur: Duration, + ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)> { + let MutexGuard { _mutex, inner } = guard; + + map_lockresult(self.0.wait_timeout(inner, dur), |(inner, result)| { + (MutexGuard { _mutex, inner }, result) + }) + } + + /// Wrapper for [`std::sync::Condvar::wait_timeout_while`]. + pub fn wait_timeout_while<'a, T, F>( + &self, + guard: MutexGuard<'a, T>, + dur: Duration, + condition: F, + ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)> + where + F: FnMut(&mut T) -> bool, + { + let MutexGuard { _mutex, inner } = guard; + + map_lockresult( + self.0.wait_timeout_while(inner, dur, condition), + |(inner, result)| (MutexGuard { _mutex, inner }, result), + ) + } + + /// Wrapper for [`std::sync::Condvar::notify_one`]. + pub fn notify_one(&self) { + self.0.notify_one(); + } + + /// Wrapper for [`std::sync::Condvar::notify_all`]. + pub fn notify_all(&self) { + self.0.notify_all(); + } +} + +/// Wrapper for [`std::sync::RwLock`]. +#[derive(Debug, Default)] +pub struct RwLock { + inner: sync::RwLock, + id: LazyMutexId, +} + +/// Hybrid wrapper for both [`std::sync::RwLockReadGuard`] and [`std::sync::RwLockWriteGuard`]. +/// +/// Please refer to [`RwLockReadGuard`] and [`RwLockWriteGuard`] for usable types. +#[derive(Debug)] +pub struct TracingRwLockGuard<'a, L> { + inner: L, + _mutex: BorrowedMutex<'a>, +} + +/// Wrapper around [`std::sync::RwLockReadGuard`]. +pub type RwLockReadGuard<'a, T> = TracingRwLockGuard<'a, sync::RwLockReadGuard<'a, T>>; +/// Wrapper around [`std::sync::RwLockWriteGuard`]. +pub type RwLockWriteGuard<'a, T> = TracingRwLockGuard<'a, sync::RwLockWriteGuard<'a, T>>; + +impl RwLock { + pub const fn new(t: T) -> Self { + Self { + inner: sync::RwLock::new(t), + id: LazyMutexId::new(), + } + } + + /// Wrapper for [`std::sync::RwLock::read`]. + /// + /// # Panics + /// + /// This method participates in lock dependency tracking. If acquiring this lock introduces a + /// dependency cycle, this method will panic. + #[track_caller] + pub fn read(&self) -> LockResult> { + let mutex = self.id.get_borrowed(); + let result = self.inner.read(); + + map_lockresult(result, |inner| TracingRwLockGuard { + inner, + _mutex: mutex, + }) + } + + /// Wrapper for [`std::sync::RwLock::write`]. + /// + /// # Panics + /// + /// This method participates in lock dependency tracking. If acquiring this lock introduces a + /// dependency cycle, this method will panic. + #[track_caller] + pub fn write(&self) -> LockResult> { + let mutex = self.id.get_borrowed(); + let result = self.inner.write(); + + map_lockresult(result, |inner| TracingRwLockGuard { + inner, + _mutex: mutex, + }) + } + + /// Wrapper for [`std::sync::RwLock::try_read`]. + /// + /// # Panics + /// + /// This method participates in lock dependency tracking. If acquiring this lock introduces a + /// dependency cycle, this method will panic. + #[track_caller] + pub fn try_read(&self) -> TryLockResult> { + let mutex = self.id.get_borrowed(); + let result = self.inner.try_read(); + + map_trylockresult(result, |inner| TracingRwLockGuard { + inner, + _mutex: mutex, + }) + } + + /// Wrapper for [`std::sync::RwLock::try_write`]. + /// + /// # Panics + /// + /// This method participates in lock dependency tracking. If acquiring this lock introduces a + /// dependency cycle, this method will panic. + #[track_caller] + pub fn try_write(&self) -> TryLockResult> { + let mutex = self.id.get_borrowed(); + let result = self.inner.try_write(); + + map_trylockresult(result, |inner| TracingRwLockGuard { + inner, + _mutex: mutex, + }) + } + + /// Return a mutable reference to the underlying data. + /// + /// This method does not block as the locking is handled compile-time by the type system. + pub fn get_mut(&mut self) -> LockResult<&mut T> { + self.inner.get_mut() + } + + /// Unwrap the mutex and return its inner value. + pub fn into_inner(self) -> LockResult { + self.inner.into_inner() + } +} + +impl From for RwLock { + fn from(t: T) -> Self { + Self::new(t) + } +} + +impl Deref for TracingRwLockGuard<'_, L> +where + L: Deref, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +impl DerefMut for TracingRwLockGuard<'_, L> +where + L: Deref + DerefMut, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner.deref_mut() + } +} + +/// Wrapper around [`std::sync::Once`]. +/// +/// Refer to the [crate-level][`crate`] documentaiton for the differences between this struct +/// and the one it wraps. +#[derive(Debug)] +pub struct Once { + inner: sync::Once, + mutex_id: LazyMutexId, +} + +// New without default is intentional, `std::sync::Once` doesn't implement it either +#[allow(clippy::new_without_default)] +impl Once { + /// Create a new `Once` value. + pub const fn new() -> Self { + Self { + inner: sync::Once::new(), + mutex_id: LazyMutexId::new(), + } + } + + /// Wrapper for [`std::sync::Once::call_once`]. + /// + /// # Panics + /// + /// In addition to the panics that `Once` can cause, this method will panic if calling it + /// introduces a cycle in the lock dependency graph. + pub fn call_once(&self, f: F) + where + F: FnOnce(), + { + self.mutex_id.with_held(|| self.inner.call_once(f)) + } + + /// Performs the same operation as [`call_once`][Once::call_once] except it ignores + /// poisoning. + /// + /// # Panics + /// + /// This method participates in lock dependency tracking. If acquiring this lock introduces a + /// dependency cycle, this method will panic. + pub fn call_once_force(&self, f: F) + where + F: FnOnce(&OnceState), + { + self.mutex_id.with_held(|| self.inner.call_once_force(f)) + } + + /// Returns true if some `call_once` has completed successfully. + pub fn is_completed(&self) -> bool { + self.inner.is_completed() + } +} + +/// Wrapper for [`std::sync::OnceLock`] +/// +/// The exact locking behaviour of [`std::sync::OnceLock`] is currently undefined, but may +/// deadlock in the event of reentrant initialization attempts. This wrapper participates in +/// cycle detection as normal and will therefore panic in the event of reentrancy. +/// +/// Most of this primitive's methods do not involve locking and as such are simply passed +/// through to the inner implementation. +/// +/// # Examples +/// +/// ``` +/// use tracing_mutex::stdsync::tracing::OnceLock; +/// +/// static LOCK: OnceLock = OnceLock::new(); +/// assert!(LOCK.get().is_none()); +/// +/// std::thread::spawn(|| { +/// let value: &i32 = LOCK.get_or_init(|| 42); +/// assert_eq!(value, &42); +/// }).join().unwrap(); +/// +/// let value: Option<&i32> = LOCK.get(); +/// assert_eq!(value, Some(&42)); +/// ``` +#[derive(Debug)] +pub struct OnceLock { + id: LazyMutexId, + inner: sync::OnceLock, +} + +// N.B. this impl inlines everything that directly calls the inner implementation as there +// should be 0 overhead to doing so. +impl OnceLock { + /// Creates a new empty cell + pub const fn new() -> Self { + Self { + id: LazyMutexId::new(), + inner: sync::OnceLock::new(), + } + } + + /// Gets a reference to the underlying value. + /// + /// This method does not attempt to lock and therefore does not participate in cycle + /// detection. + #[inline] + pub fn get(&self) -> Option<&T> { + self.inner.get() + } + + /// Gets a mutable reference to the underlying value. + /// + /// This method does not attempt to lock and therefore does not participate in cycle + /// detection. + #[inline] + pub fn get_mut(&mut self) -> Option<&mut T> { + self.inner.get_mut() + } + + /// Sets the contents of this cell to the underlying value + /// + /// As this method may block until initialization is complete, it participates in cycle + /// detection. + pub fn set(&self, value: T) -> Result<(), T> { + self.id.with_held(|| self.inner.set(value)) + } + + /// Gets the contents of the cell, initializing it with `f` if the cell was empty. + /// + /// This method participates in cycle detection. Reentrancy is considered a cycle. + pub fn get_or_init(&self, f: F) -> &T + where + F: FnOnce() -> T, + { + self.id.with_held(|| self.inner.get_or_init(f)) + } + + /// Takes the value out of this `OnceLock`, moving it back to an uninitialized state. + /// + /// This method does not attempt to lock and therefore does not participate in cycle + /// detection. + #[inline] + pub fn take(&mut self) -> Option { + self.inner.take() + } + + /// Consumes the `OnceLock`, returning the wrapped value. Returns None if the cell was + /// empty. + /// + /// This method does not attempt to lock and therefore does not participate in cycle + /// detection. + #[inline] + pub fn into_inner(mut self) -> Option { + self.take() + } +} + +impl Default for OnceLock { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl PartialEq for OnceLock { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + +impl Eq for OnceLock {} + +impl Clone for OnceLock { + fn clone(&self) -> Self { + Self { + id: LazyMutexId::new(), + inner: self.inner.clone(), + } + } +} + +impl From for OnceLock { + #[inline] + fn from(value: T) -> Self { + Self { + id: LazyMutexId::new(), + inner: sync::OnceLock::from(value), + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::thread; + + use super::*; + + #[test] + fn test_mutex_usage() { + let mutex = Arc::new(Mutex::new(0)); + + assert_eq!(*mutex.lock().unwrap(), 0); + *mutex.lock().unwrap() = 1; + assert_eq!(*mutex.lock().unwrap(), 1); + + let mutex_clone = mutex.clone(); + + let _guard = mutex.lock().unwrap(); + + // Now try to cause a blocking exception in another thread + let handle = thread::spawn(move || { + let result = mutex_clone.try_lock().unwrap_err(); + + assert!(matches!(result, TryLockError::WouldBlock)); + }); + + handle.join().unwrap(); + } + + #[test] + fn test_rwlock_usage() { + let rwlock = Arc::new(RwLock::new(0)); + + assert_eq!(*rwlock.read().unwrap(), 0); + assert_eq!(*rwlock.write().unwrap(), 0); + *rwlock.write().unwrap() = 1; + assert_eq!(*rwlock.read().unwrap(), 1); + assert_eq!(*rwlock.write().unwrap(), 1); + + let rwlock_clone = rwlock.clone(); + + let _read_lock = rwlock.read().unwrap(); + + // Now try to cause a blocking exception in another thread + let handle = thread::spawn(move || { + let write_result = rwlock_clone.try_write().unwrap_err(); + + assert!(matches!(write_result, TryLockError::WouldBlock)); + + // Should be able to get a read lock just fine. + let _read_lock = rwlock_clone.read().unwrap(); + }); + + handle.join().unwrap(); + } + + #[test] + fn test_once_usage() { + let once = Arc::new(Once::new()); + let once_clone = once.clone(); + + assert!(!once.is_completed()); + + let handle = thread::spawn(move || { + assert!(!once_clone.is_completed()); + + once_clone.call_once(|| {}); + + assert!(once_clone.is_completed()); + }); + + handle.join().unwrap(); + + assert!(once.is_completed()); + } + + #[test] + #[should_panic(expected = "Found cycle in mutex dependency graph")] + fn test_detect_cycle() { + let a = Mutex::new(()); + let b = Mutex::new(()); + + let hold_a = a.lock().unwrap(); + let _ = b.lock(); + + drop(hold_a); + + let _hold_b = b.lock().unwrap(); + let _ = a.lock(); + } +} From d1a6b93ea8449f9e2df74c95d2acf0f757c29e4d Mon Sep 17 00:00:00 2001 From: Bert Peters Date: Sat, 1 Mar 2025 13:25:20 +0100 Subject: [PATCH 2/4] Add new reset_dependencies API --- src/lib.rs | 1 + src/lockapi.rs | 8 ++++++ src/stdsync/tracing.rs | 25 ++++++++++++++++++ src/util.rs | 59 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 src/util.rs diff --git a/src/lib.rs b/src/lib.rs index 4c428e9..3bb51ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,7 @@ pub mod lockapi; pub mod parkinglot; mod reporting; pub mod stdsync; +pub mod util; thread_local! { /// Stack to track which locks are held diff --git a/src/lockapi.rs b/src/lockapi.rs index 513dc60..5565d53 100644 --- a/src/lockapi.rs +++ b/src/lockapi.rs @@ -23,7 +23,9 @@ use lock_api::RawRwLockUpgradeDowngrade; use lock_api::RawRwLockUpgradeFair; use lock_api::RawRwLockUpgradeTimed; +use crate::util::PrivateTraced; use crate::LazyMutexId; +use crate::MutexId; /// Tracing wrapper for all [`lock_api`] traits. /// @@ -86,6 +88,12 @@ impl TracingWrapper { } } +impl PrivateTraced for TracingWrapper { + fn get_id(&self) -> &MutexId { + &self.id + } +} + unsafe impl RawMutex for TracingWrapper where T: RawMutex, diff --git a/src/stdsync/tracing.rs b/src/stdsync/tracing.rs index afc6998..a47d493 100644 --- a/src/stdsync/tracing.rs +++ b/src/stdsync/tracing.rs @@ -10,6 +10,7 @@ use std::sync::TryLockResult; use std::sync::WaitTimeoutResult; use std::time::Duration; +use crate::util::PrivateTraced; use crate::BorrowedMutex; use crate::LazyMutexId; @@ -127,6 +128,12 @@ impl Mutex { } } +impl PrivateTraced for Mutex { + fn get_id(&self) -> &crate::MutexId { + &self.id + } +} + impl From for Mutex { fn from(t: T) -> Self { Self::new(t) @@ -375,6 +382,12 @@ impl RwLock { } } +impl PrivateTraced for RwLock { + fn get_id(&self) -> &crate::MutexId { + &self.id + } +} + impl From for RwLock { fn from(t: T) -> Self { Self::new(t) @@ -455,6 +468,12 @@ impl Once { } } +impl PrivateTraced for Once { + fn get_id(&self) -> &crate::MutexId { + &self.mutex_id + } +} + /// Wrapper for [`std::sync::OnceLock`] /// /// The exact locking behaviour of [`std::sync::OnceLock`] is currently undefined, but may @@ -553,6 +572,12 @@ impl OnceLock { } } +impl PrivateTraced for OnceLock { + fn get_id(&self) -> &crate::MutexId { + &self.id + } +} + impl Default for OnceLock { #[inline] fn default() -> Self { diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..6e12432 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,59 @@ +use crate::MutexId; + +/// Reset the dependencies for the given entity. +/// +/// # Performance +/// +/// This function locks the dependency graph to remove the item from it. This is an `O(E)` operation +/// with `E` being the number of dependencies directly associated with this particular instance. As +/// such, it is not advisable to call this method from a hot loop. +/// +/// # Safety +/// +/// Use of this method invalidates the deadlock prevention guarantees that this library makes. As +/// such, it should only be used when it is absolutely certain this will not introduce deadlocks +/// later. +/// +/// Other than deadlocks, no undefined behaviour can result from the use of this function. +/// +/// # Example +/// +/// ``` +/// use tracing_mutex::stdsync::Mutex; +/// use tracing_mutex::util; +/// +/// let first = Mutex::new(()); +/// let second = Mutex::new(()); +/// +/// { +/// let _first_lock = first.lock().unwrap(); +/// second.lock().unwrap(); +/// } +/// +/// // Reset the dependencies for the first mutex +/// unsafe { util::reset_dependencies(&first) }; +/// +/// // Now we can unlock the mutexes in the opposite order without a panic. +/// let _second_lock = second.lock().unwrap(); +/// first.lock().unwrap(); +/// ``` +pub unsafe fn reset_dependencies(traced: &T) { + crate::get_dependency_graph().remove_node(traced.get_id().value()); +} + +/// Types that participate in dependency tracking +/// +/// This trait is a public marker trait and is automatically implemented fore all types that +/// implement the internal dependency tracking features. +#[allow(private_bounds)] +pub trait Traced: PrivateTraced {} + +impl Traced for T {} + +/// Private implementation of the traced marker. +/// +/// This trait is private (and seals the outer trait) to avoid exposing the MutexId type. +pub(crate) trait PrivateTraced { + /// Get the mutex id associated with this traced item. + fn get_id(&self) -> &MutexId; +} From 1ad0b2756e902f0ddf8eee531a828b0a8408cc25 Mon Sep 17 00:00:00 2001 From: Bert Peters Date: Mon, 10 Mar 2025 18:31:47 +0100 Subject: [PATCH 3/4] Rework CI for experimental features --- .github/workflows/ci.yml | 4 +++- Cargo.toml | 1 + src/util.rs | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52a7ab1..b78d0f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ jobs: strategy: matrix: rust: + - "1.74" # Current minimum for experimental features - stable - beta - nightly @@ -44,7 +45,8 @@ jobs: with: toolchain: "1.70" - - run: cargo test --all-features + # Test everything except experimental features. + - run: cargo test --features backtraces,lockapi,parkinglot docs: name: Documentation build diff --git a/Cargo.toml b/Cargo.toml index fc8bf29..8a1fc88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ required-features = ["parkinglot"] [features] default = ["backtraces"] backtraces = [] +experimental = [] # Feature names do not match crate names pending namespaced features. lockapi = ["lock_api"] parkinglot = ["parking_lot", "lockapi"] diff --git a/src/util.rs b/src/util.rs index 6e12432..f4ea6a3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -37,6 +37,8 @@ use crate::MutexId; /// let _second_lock = second.lock().unwrap(); /// first.lock().unwrap(); /// ``` +#[cfg(feature = "experimental")] +#[cfg_attr(docsrs, doc(cfg(feature = "experimental")))] pub unsafe fn reset_dependencies(traced: &T) { crate::get_dependency_graph().remove_node(traced.get_id().value()); } @@ -45,14 +47,19 @@ pub unsafe fn reset_dependencies(traced: &T) { /// /// This trait is a public marker trait and is automatically implemented fore all types that /// implement the internal dependency tracking features. +#[cfg(feature = "experimental")] +#[cfg_attr(docsrs, doc(cfg(feature = "experimental")))] #[allow(private_bounds)] pub trait Traced: PrivateTraced {} +#[cfg(feature = "experimental")] +#[cfg_attr(docsrs, doc(cfg(feature = "experimental")))] impl Traced for T {} /// Private implementation of the traced marker. /// /// This trait is private (and seals the outer trait) to avoid exposing the MutexId type. +#[cfg_attr(not(feature = "experimental"), allow(unused))] pub(crate) trait PrivateTraced { /// Get the mutex id associated with this traced item. fn get_id(&self) -> &MutexId; From af366ca3af03f872ec8ae8994a7882d8d21e1390 Mon Sep 17 00:00:00 2001 From: Bert Peters Date: Mon, 10 Mar 2025 19:32:24 +0100 Subject: [PATCH 4/4] Document new functionality --- CHANGELOG.md | 8 ++++++++ src/lib.rs | 7 ++++++- src/util.rs | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9472c..c13dc62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `tracing_mutex::parkinglot` is now identical to `parking_lot`, and an example showing how to use it as a drop-in replacement was added. +- Introduced `experimental` feature, which will be used going forward to help evolve the API outside + the normal stability guarantees. APIs under this feature are not subject to semver or MSRV + guarantees. + +- Added experimental api `tracing_mutex::util::reset_dependencies`, which can be used to reset the + ordering information of a specific mutex when you need to reorder them. This API is unsafe, as its + use invalidates any of the deadlock prevention guarantees made. + ### Changed - Reworked CI to better test continued support for the minimum supported Rust version diff --git a/src/lib.rs b/src/lib.rs index 3bb51ce..6a446ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,12 +30,17 @@ //! //! - `backtraces`: Enables capturing backtraces of mutex dependencies, to make it easier to //! determine what sequence of events would trigger a deadlock. This is enabled by default, but if -//! the performance overhead is unaccceptable, it can be disabled by disabling default features. +//! the performance overhead is unacceptable, it can be disabled by disabling default features. //! //! - `lockapi`: Enables the wrapper lock for [`lock_api`][lock_api] locks //! //! - `parkinglot`: Enables wrapper types for [`parking_lot`][parking_lot] mutexes //! +//! - `experimental`: Enables experimental features. Experimental features are intended to test new +//! APIs and play with new APIs before committing to them. As such, breaking changes may be +//! introduced in it between otherwise semver-compatible versions, and the MSRV does not apply to +//! experimental features. +//! //! # Performance considerations //! //! Tracing a mutex adds overhead to certain mutex operations in order to do the required diff --git a/src/util.rs b/src/util.rs index f4ea6a3..a382802 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,4 @@ +//! Utilities related to the internals of dependency tracking. use crate::MutexId; /// Reset the dependencies for the given entity.