mirror of
https://github.com/bertptrs/tracing-mutex.git
synced 2025-12-27 21:40:32 +01:00
Compare commits
6 Commits
v0.3.0
...
21b88fee18
| Author | SHA1 | Date | |
|---|---|---|---|
| 21b88fee18 | |||
| ec9fcf17ca | |||
| 9ca5af2c82 | |||
| 74b4fe0bb1 | |||
|
|
6199598944 | ||
| fd75fc453b |
@@ -36,3 +36,6 @@ backtraces = []
|
|||||||
# Feature names do not match crate names pending namespaced features.
|
# Feature names do not match crate names pending namespaced features.
|
||||||
lockapi = ["lock_api"]
|
lockapi = ["lock_api"]
|
||||||
parkinglot = ["parking_lot", "lockapi"]
|
parkinglot = ["parking_lot", "lockapi"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
autocfg = "1.4.0"
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -56,10 +56,10 @@ introduce a cyclic dependency between your locks, the operation panics instead.
|
|||||||
immediately notice the cyclic dependency rather than be eventually surprised by it in production.
|
immediately notice the cyclic dependency rather than be eventually surprised by it in production.
|
||||||
|
|
||||||
Mutex tracing is efficient, but it is not completely overhead-free. If you cannot spare the
|
Mutex tracing is efficient, but it is not completely overhead-free. If you cannot spare the
|
||||||
performance penalty in your production environment, this library also offers debug-only tracing.
|
performance penalty in your production environment, this library also offers debug-only tracing. The
|
||||||
`DebugMutex`, also found in the `stdsync` module, is a type alias that evaluates to `TracingMutex`
|
type aliases in `tracing_mutex::stdsync` correspond to tracing primitives from
|
||||||
when debug assertions are enabled, and to `Mutex` when they are not. Similar helper types are
|
`tracing_mutex::stdsync::tracing` when debug assertions are enabled, and to primitives from
|
||||||
available for other synchronization primitives.
|
`std::sync::Mutex` when they are not. A similar structure exists for other
|
||||||
|
|
||||||
The minimum supported Rust version is 1.70. Increasing this is not considered a breaking change, but
|
The minimum supported Rust version is 1.70. Increasing this is not considered a breaking change, but
|
||||||
will be avoided within semver-compatible releases if possible.
|
will be avoided within semver-compatible releases if possible.
|
||||||
@@ -68,6 +68,7 @@ will be avoided within semver-compatible releases if possible.
|
|||||||
|
|
||||||
- Dependency-tracking wrappers for all locking primitives
|
- Dependency-tracking wrappers for all locking primitives
|
||||||
- Optional opt-out for release mode code
|
- Optional opt-out for release mode code
|
||||||
|
- Optional backtrace capture to aid with reproducing cyclic mutex chains
|
||||||
- Support for primitives from:
|
- Support for primitives from:
|
||||||
- `std::sync`
|
- `std::sync`
|
||||||
- `parking_lot`
|
- `parking_lot`
|
||||||
@@ -76,7 +77,6 @@ will be avoided within semver-compatible releases if possible.
|
|||||||
## Future improvements
|
## Future improvements
|
||||||
|
|
||||||
- Improve performance in lock tracing
|
- Improve performance in lock tracing
|
||||||
- Optional logging to make debugging easier
|
|
||||||
- Better and configurable error handling when detecting cyclic dependencies
|
- Better and configurable error handling when detecting cyclic dependencies
|
||||||
- Support for other locking libraries
|
- Support for other locking libraries
|
||||||
- Support for async locking libraries
|
- Support for async locking libraries
|
||||||
|
|||||||
10
build.rs
Normal file
10
build.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use autocfg::AutoCfg;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// To avoid bumping MSRV unnecessarily, we can sniff certain features. Reevaluate this on major
|
||||||
|
// releases.
|
||||||
|
let ac = AutoCfg::new().unwrap();
|
||||||
|
ac.emit_has_path("std::sync::LazyLock");
|
||||||
|
|
||||||
|
autocfg::rerun_path("build.rs");
|
||||||
|
}
|
||||||
@@ -1,26 +1,62 @@
|
|||||||
//! Show what a crash looks like
|
//! Show what a crash looks like
|
||||||
//!
|
//!
|
||||||
//! This shows what a traceback of a cycle detection looks like. It is expected to crash.
|
//! This shows what a traceback of a cycle detection looks like. It is expected to crash when run in
|
||||||
|
//! debug mode, because it might deadlock. In release mode, no tracing is used and the program may
|
||||||
|
//! do any of the following:
|
||||||
|
//!
|
||||||
|
//! - Return a random valuation of `a`, `b`, and `c`. The implementation has a race-condition by
|
||||||
|
//! design. I have observed (4, 3, 6), but also (6, 3, 5).
|
||||||
|
//! - Deadlock forever.
|
||||||
|
//!
|
||||||
|
//! One can increase the SLEEP_TIME constant to increase the likelihood of a deadlock to occur. On
|
||||||
|
//! my machine, 1ns of sleep time gives about a 50/50 chance of the program deadlocking.
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use tracing_mutex::stdsync::Mutex;
|
use tracing_mutex::stdsync::Mutex;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let a = Mutex::new(());
|
let a = Mutex::new(1);
|
||||||
let b = Mutex::new(());
|
let b = Mutex::new(2);
|
||||||
let c = Mutex::new(());
|
let c = Mutex::new(3);
|
||||||
|
|
||||||
// Create an edge from a to b
|
// Increase this time to increase the likelihood of a deadlock.
|
||||||
{
|
const SLEEP_TIME: Duration = Duration::from_nanos(1);
|
||||||
let _a = a.lock();
|
|
||||||
let _b = b.lock();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an edge from b to c
|
// Depending on random CPU performance, this section may deadlock, or may return a result. With
|
||||||
{
|
// tracing enabled, the potential deadlock is always detected and a backtrace should be
|
||||||
let _b = b.lock();
|
// produced.
|
||||||
let _c = c.lock();
|
thread::scope(|s| {
|
||||||
}
|
// Create an edge from a to b
|
||||||
|
s.spawn(|| {
|
||||||
|
let a = a.lock().unwrap();
|
||||||
|
thread::sleep(SLEEP_TIME);
|
||||||
|
*b.lock().unwrap() += *a;
|
||||||
|
});
|
||||||
|
|
||||||
// Now crash by trying to add an edge from c to a
|
// Create an edge from b to c
|
||||||
let _c = c.lock();
|
s.spawn(|| {
|
||||||
let _a = a.lock(); // This line will crash
|
let b = b.lock().unwrap();
|
||||||
|
thread::sleep(SLEEP_TIME);
|
||||||
|
*c.lock().unwrap() += *b;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an edge from c to a
|
||||||
|
//
|
||||||
|
// N.B. the program can crash on any of the three edges, as there is no guarantee which
|
||||||
|
// thread will execute first. Nevertheless, any one of them is guaranteed to panic with
|
||||||
|
// tracing enabled.
|
||||||
|
s.spawn(|| {
|
||||||
|
let c = c.lock().unwrap();
|
||||||
|
thread::sleep(SLEEP_TIME);
|
||||||
|
*a.lock().unwrap() += *c;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}, {}, {}",
|
||||||
|
a.into_inner().unwrap(),
|
||||||
|
b.into_inner().unwrap(),
|
||||||
|
c.into_inner().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,14 @@ impl MutexId {
|
|||||||
unreachable!("Tried to drop lock for mutex {:?} but it wasn't held", self)
|
unreachable!("Tried to drop lock for mutex {:?} but it wasn't held", self)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute the given closure while the guard is held.
|
||||||
|
pub fn with_held<T>(&self, f: impl FnOnce() -> T) -> T {
|
||||||
|
// Note: we MUST construct the RAII guard, we cannot simply mark held + mark released, as
|
||||||
|
// f() may panic and corrupt our state.
|
||||||
|
let _guard = self.get_borrowed();
|
||||||
|
f()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MutexId {
|
impl Default for MutexId {
|
||||||
|
|||||||
@@ -138,16 +138,14 @@ pub mod tracing {
|
|||||||
/// This method will panic if `f` panics, poisoning this `Once`. In addition, this function
|
/// This method will panic if `f` panics, poisoning this `Once`. In addition, this function
|
||||||
/// panics when the lock acquisition order is determined to be inconsistent.
|
/// panics when the lock acquisition order is determined to be inconsistent.
|
||||||
pub fn call_once(&self, f: impl FnOnce()) {
|
pub fn call_once(&self, f: impl FnOnce()) {
|
||||||
let _borrow = self.id.get_borrowed();
|
self.id.with_held(|| self.inner.call_once(f));
|
||||||
self.inner.call_once(f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs the given initialization routine once and only once.
|
/// Performs the given initialization routine once and only once.
|
||||||
///
|
///
|
||||||
/// This method is identical to [`Once::call_once`] except it ignores poisoning.
|
/// This method is identical to [`Once::call_once`] except it ignores poisoning.
|
||||||
pub fn call_once_force(&self, f: impl FnOnce(OnceState)) {
|
pub fn call_once_force(&self, f: impl FnOnce(OnceState)) {
|
||||||
let _borrow = self.id.get_borrowed();
|
self.id.with_held(|| self.inner.call_once_force(f));
|
||||||
self.inner.call_once_force(f);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ pub use tracing::{
|
|||||||
Condvar, Mutex, MutexGuard, Once, OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard,
|
Condvar, Mutex, MutexGuard, Once, OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(all(has_std__sync__LazyLock, debug_assertions))]
|
||||||
|
pub use tracing::LazyLock;
|
||||||
|
|
||||||
|
#[cfg(all(has_std__sync__LazyLock, not(debug_assertions)))]
|
||||||
|
pub use std::sync::LazyLock;
|
||||||
|
|
||||||
/// Dependency tracing versions of [`std::sync`].
|
/// Dependency tracing versions of [`std::sync`].
|
||||||
pub mod tracing {
|
pub mod tracing {
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -47,6 +53,12 @@ pub mod tracing {
|
|||||||
use crate::BorrowedMutex;
|
use crate::BorrowedMutex;
|
||||||
use crate::LazyMutexId;
|
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`].
|
/// Wrapper for [`std::sync::Mutex`].
|
||||||
///
|
///
|
||||||
/// Refer to the [crate-level][`crate`] documentation for the differences between this struct and
|
/// Refer to the [crate-level][`crate`] documentation for the differences between this struct and
|
||||||
@@ -458,8 +470,7 @@ pub mod tracing {
|
|||||||
where
|
where
|
||||||
F: FnOnce(),
|
F: FnOnce(),
|
||||||
{
|
{
|
||||||
let _guard = self.mutex_id.get_borrowed();
|
self.mutex_id.with_held(|| self.inner.call_once(f))
|
||||||
self.inner.call_once(f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs the same operation as [`call_once`][Once::call_once] except it ignores
|
/// Performs the same operation as [`call_once`][Once::call_once] except it ignores
|
||||||
@@ -473,8 +484,7 @@ pub mod tracing {
|
|||||||
where
|
where
|
||||||
F: FnOnce(&OnceState),
|
F: FnOnce(&OnceState),
|
||||||
{
|
{
|
||||||
let _guard = self.mutex_id.get_borrowed();
|
self.mutex_id.with_held(|| self.inner.call_once_force(f))
|
||||||
self.inner.call_once_force(f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if some `call_once` has completed successfully.
|
/// Returns true if some `call_once` has completed successfully.
|
||||||
@@ -548,9 +558,7 @@ pub mod tracing {
|
|||||||
/// As this method may block until initialization is complete, it participates in cycle
|
/// As this method may block until initialization is complete, it participates in cycle
|
||||||
/// detection.
|
/// detection.
|
||||||
pub fn set(&self, value: T) -> Result<(), T> {
|
pub fn set(&self, value: T) -> Result<(), T> {
|
||||||
let _guard = self.id.get_borrowed();
|
self.id.with_held(|| self.inner.set(value))
|
||||||
|
|
||||||
self.inner.set(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the contents of the cell, initializing it with `f` if the cell was empty.
|
/// Gets the contents of the cell, initializing it with `f` if the cell was empty.
|
||||||
@@ -560,8 +568,7 @@ pub mod tracing {
|
|||||||
where
|
where
|
||||||
F: FnOnce() -> T,
|
F: FnOnce() -> T,
|
||||||
{
|
{
|
||||||
let _guard = self.id.get_borrowed();
|
self.id.with_held(|| self.inner.get_or_init(f))
|
||||||
self.inner.get_or_init(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes the value out of this `OnceLock`, moving it back to an uninitialized state.
|
/// Takes the value out of this `OnceLock`, moving it back to an uninitialized state.
|
||||||
|
|||||||
113
src/stdsync/tracing/lazy_lock.rs
Normal file
113
src/stdsync/tracing/lazy_lock.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
//! Wrapper implementation for LazyLock
|
||||||
|
//!
|
||||||
|
//! This lives in a separate module as LazyLock would otherwise raise our MSRV to 1.80. Reevaluate
|
||||||
|
//! this in the future.
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use crate::LazyMutexId;
|
||||||
|
|
||||||
|
/// Wrapper for [`std::sync::LazyLock`]
|
||||||
|
///
|
||||||
|
/// This wrapper participates in cycle detection like all other primitives in this crate. It should
|
||||||
|
/// only be possible to encounter cycles when acquiring mutexes in the initialisation function.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use tracing_mutex::stdsync::tracing::LazyLock;
|
||||||
|
///
|
||||||
|
/// static LOCK: LazyLock<i32> = LazyLock::new(|| {
|
||||||
|
/// println!("Hello, world!");
|
||||||
|
/// 42
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// // This should print "Hello, world!"
|
||||||
|
/// println!("{}", *LOCK);
|
||||||
|
/// // This should not.
|
||||||
|
/// println!("{}", *LOCK);
|
||||||
|
/// ```
|
||||||
|
pub struct LazyLock<T, F = fn() -> T> {
|
||||||
|
inner: std::sync::LazyLock<T, F>,
|
||||||
|
id: LazyMutexId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F: FnOnce() -> T> LazyLock<T, F> {
|
||||||
|
/// Creates a new lazy value with the given initializing function.
|
||||||
|
pub const fn new(f: F) -> LazyLock<T, F> {
|
||||||
|
Self {
|
||||||
|
id: LazyMutexId::new(),
|
||||||
|
inner: std::sync::LazyLock::new(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force this lazy lock to be evaluated.
|
||||||
|
///
|
||||||
|
/// This is equivalent to dereferencing, but is more explicit.
|
||||||
|
pub fn force(this: &LazyLock<T, F>) -> &T {
|
||||||
|
&*this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, F: FnOnce() -> T> Deref for LazyLock<T, F> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.id.with_held(|| &*self.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Default> Default for LazyLock<T> {
|
||||||
|
/// Return a `LazyLock` that is initialized through [`Default`].
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(Default::default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug, F> Debug for LazyLock<T, F> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
// Cannot implement this ourselves because the get() used is nightly, so delegate.
|
||||||
|
self.inner.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::stdsync::Mutex;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_only_init_once() {
|
||||||
|
let mut init_counter = 0;
|
||||||
|
|
||||||
|
let lock = LazyLock::new(|| {
|
||||||
|
init_counter += 1;
|
||||||
|
42
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(*lock, 42);
|
||||||
|
LazyLock::force(&lock);
|
||||||
|
|
||||||
|
// Ensure we can access the init counter
|
||||||
|
drop(lock);
|
||||||
|
|
||||||
|
assert_eq!(init_counter, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Found cycle")]
|
||||||
|
fn test_panic_with_cycle() {
|
||||||
|
let mutex = Mutex::new(());
|
||||||
|
|
||||||
|
let lock = LazyLock::new(|| *mutex.lock().unwrap());
|
||||||
|
|
||||||
|
// Establish the relation from lock to mutex
|
||||||
|
LazyLock::force(&lock);
|
||||||
|
|
||||||
|
// Now do it the other way around, which should crash
|
||||||
|
let _guard = mutex.lock().unwrap();
|
||||||
|
LazyLock::force(&lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user