mirror of
https://github.com/bertptrs/tracing-mutex.git
synced 2025-12-25 20:50:32 +01:00
Show cycle backtraces when they happen
This commit is contained in:
@@ -11,6 +11,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
- The minimum supported Rust version is now defined as 1.70. Previously it was undefined.
|
- The minimum supported Rust version is now defined as 1.70. Previously it was undefined.
|
||||||
- Wrappers for `std::sync` primitives can now be `const` constructed.
|
- Wrappers for `std::sync` primitives can now be `const` constructed.
|
||||||
- Add support for `std::sync::OnceLock`
|
- Add support for `std::sync::OnceLock`
|
||||||
|
- Added backtraces of mutex allocations to the cycle report.
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|
||||||
|
|||||||
26
examples/mutex_cycle.rs
Normal file
26
examples/mutex_cycle.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//! Show what a crash looks like
|
||||||
|
//!
|
||||||
|
//! This shows what a traceback of a cycle detection looks like. It is expected to crash.
|
||||||
|
use tracing_mutex::stdsync::Mutex;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let a = Mutex::new(());
|
||||||
|
let b = Mutex::new(());
|
||||||
|
let c = Mutex::new(());
|
||||||
|
|
||||||
|
// Create an edge from a to b
|
||||||
|
{
|
||||||
|
let _a = a.lock();
|
||||||
|
let _b = b.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an edge from b to c
|
||||||
|
{
|
||||||
|
let _b = b.lock();
|
||||||
|
let _c = c.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now crash by trying to add an edge from c to a
|
||||||
|
let _c = c.lock();
|
||||||
|
let _a = a.lock(); // This line will crash
|
||||||
|
}
|
||||||
59
src/graph.rs
59
src/graph.rs
@@ -107,10 +107,18 @@ where
|
|||||||
///
|
///
|
||||||
/// Nodes, both from and to, are created as needed when creating new edges. If the new edge
|
/// Nodes, both from and to, are created as needed when creating new edges. If the new edge
|
||||||
/// would introduce a cycle, the edge is rejected and `false` is returned.
|
/// would introduce a cycle, the edge is rejected and `false` is returned.
|
||||||
pub(crate) fn add_edge(&mut self, x: V, y: V, e: impl FnOnce() -> E) -> bool {
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If the edge would introduce the cycle, the underlying graph is not modified and a list of
|
||||||
|
/// all the edge data in the would-be cycle is returned instead.
|
||||||
|
pub(crate) fn add_edge(&mut self, x: V, y: V, e: impl FnOnce() -> E) -> Result<(), Vec<E>>
|
||||||
|
where
|
||||||
|
E: Clone,
|
||||||
|
{
|
||||||
if x == y {
|
if x == y {
|
||||||
// self-edges are always considered cycles
|
// self-edges are always considered cycles
|
||||||
return false;
|
return Err(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_, out_edges, ub) = self.add_node(x);
|
let (_, out_edges, ub) = self.add_node(x);
|
||||||
@@ -118,7 +126,7 @@ where
|
|||||||
match out_edges.entry(y) {
|
match out_edges.entry(y) {
|
||||||
Entry::Occupied(_) => {
|
Entry::Occupied(_) => {
|
||||||
// Edge already exists, nothing to be done
|
// Edge already exists, nothing to be done
|
||||||
return true;
|
return Ok(());
|
||||||
}
|
}
|
||||||
Entry::Vacant(entry) => entry.insert(e()),
|
Entry::Vacant(entry) => entry.insert(e()),
|
||||||
};
|
};
|
||||||
@@ -133,7 +141,7 @@ where
|
|||||||
let mut delta_f = Vec::new();
|
let mut delta_f = Vec::new();
|
||||||
let mut delta_b = Vec::new();
|
let mut delta_b = Vec::new();
|
||||||
|
|
||||||
if !self.dfs_f(&self.nodes[&y], ub, &mut visited, &mut delta_f) {
|
if let Err(cycle) = self.dfs_f(&self.nodes[&y], ub, &mut visited, &mut delta_f) {
|
||||||
// This edge introduces a cycle, so we want to reject it and remove it from the
|
// This edge introduces a cycle, so we want to reject it and remove it from the
|
||||||
// graph again to keep the "does not contain cycles" invariant.
|
// graph again to keep the "does not contain cycles" invariant.
|
||||||
|
|
||||||
@@ -143,7 +151,7 @@ where
|
|||||||
self.nodes.get_mut(&x).map(|node| node.out_edges.remove(&y));
|
self.nodes.get_mut(&x).map(|node| node.out_edges.remove(&y));
|
||||||
|
|
||||||
// No edge was added
|
// No edge was added
|
||||||
return false;
|
return Err(cycle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to check as we should've found the cycle on the forward pass
|
// No need to check as we should've found the cycle on the forward pass
|
||||||
@@ -155,7 +163,7 @@ where
|
|||||||
self.reorder(delta_f, delta_b);
|
self.reorder(delta_f, delta_b);
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forwards depth-first-search
|
/// Forwards depth-first-search
|
||||||
@@ -165,25 +173,30 @@ where
|
|||||||
ub: Order,
|
ub: Order,
|
||||||
visited: &mut HashSet<V>,
|
visited: &mut HashSet<V>,
|
||||||
delta_f: &mut Vec<&'a Node<V, E>>,
|
delta_f: &mut Vec<&'a Node<V, E>>,
|
||||||
) -> bool {
|
) -> Result<(), Vec<E>>
|
||||||
|
where
|
||||||
|
E: Clone,
|
||||||
|
{
|
||||||
delta_f.push(n);
|
delta_f.push(n);
|
||||||
|
|
||||||
n.out_edges.keys().all(|w| {
|
for (w, e) in &n.out_edges {
|
||||||
let node = &self.nodes[w];
|
let node = &self.nodes[w];
|
||||||
let ord = node.ord.get();
|
let ord = node.ord.get();
|
||||||
|
|
||||||
if ord == ub {
|
if ord == ub {
|
||||||
// Found a cycle
|
// Found a cycle
|
||||||
false
|
return Err(vec![e.clone()]);
|
||||||
} else if !visited.contains(w) && ord < ub {
|
} else if !visited.contains(w) && ord < ub {
|
||||||
// Need to check recursively
|
// Need to check recursively
|
||||||
visited.insert(*w);
|
visited.insert(*w);
|
||||||
self.dfs_f(node, ub, visited, delta_f)
|
if let Err(mut chain) = self.dfs_f(node, ub, visited, delta_f) {
|
||||||
} else {
|
chain.push(e.clone());
|
||||||
// Already seen this one or not interesting
|
return Err(chain);
|
||||||
true
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backwards depth-first-search
|
/// Backwards depth-first-search
|
||||||
@@ -260,7 +273,7 @@ mod tests {
|
|||||||
// Regression test for https://github.com/bertptrs/tracing-mutex/issues/7
|
// Regression test for https://github.com/bertptrs/tracing-mutex/issues/7
|
||||||
let mut graph = DiGraph::default();
|
let mut graph = DiGraph::default();
|
||||||
|
|
||||||
assert!(!graph.add_edge(1, 1, nop));
|
assert!(graph.add_edge(1, 1, nop).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -268,16 +281,16 @@ mod tests {
|
|||||||
let mut graph = DiGraph::default();
|
let mut graph = DiGraph::default();
|
||||||
|
|
||||||
// Add some safe edges
|
// Add some safe edges
|
||||||
assert!(graph.add_edge(0, 1, nop));
|
assert!(graph.add_edge(0, 1, nop).is_ok());
|
||||||
assert!(graph.add_edge(1, 2, nop));
|
assert!(graph.add_edge(1, 2, nop).is_ok());
|
||||||
assert!(graph.add_edge(2, 3, nop));
|
assert!(graph.add_edge(2, 3, nop).is_ok());
|
||||||
assert!(graph.add_edge(4, 2, nop));
|
assert!(graph.add_edge(4, 2, nop).is_ok());
|
||||||
|
|
||||||
// Try to add an edge that introduces a cycle
|
// Try to add an edge that introduces a cycle
|
||||||
assert!(!graph.add_edge(3, 1, nop));
|
assert!(graph.add_edge(3, 1, nop).is_err());
|
||||||
|
|
||||||
// Add an edge that should reorder 0 to be after 4
|
// Add an edge that should reorder 0 to be after 4
|
||||||
assert!(graph.add_edge(4, 0, nop));
|
assert!(graph.add_edge(4, 0, nop).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fuzz the DiGraph implementation by adding a bunch of valid edges.
|
/// Fuzz the DiGraph implementation by adding a bunch of valid edges.
|
||||||
@@ -285,7 +298,7 @@ mod tests {
|
|||||||
/// This test generates all possible forward edges in a 100-node graph consisting of natural
|
/// This test generates all possible forward edges in a 100-node graph consisting of natural
|
||||||
/// numbers, shuffles them, then adds them to the graph. This will always be a valid directed,
|
/// numbers, shuffles them, then adds them to the graph. This will always be a valid directed,
|
||||||
/// acyclic graph because there is a trivial order (the natural numbers) but because the edges
|
/// acyclic graph because there is a trivial order (the natural numbers) but because the edges
|
||||||
/// are added in a random order the DiGraph will still occassionally need to reorder nodes.
|
/// are added in a random order the DiGraph will still occasionally need to reorder nodes.
|
||||||
#[test]
|
#[test]
|
||||||
fn fuzz_digraph() {
|
fn fuzz_digraph() {
|
||||||
// Note: this fuzzer is quadratic in the number of nodes, so this cannot be too large or it
|
// Note: this fuzzer is quadratic in the number of nodes, so this cannot be too large or it
|
||||||
@@ -306,7 +319,7 @@ mod tests {
|
|||||||
let mut graph = DiGraph::default();
|
let mut graph = DiGraph::default();
|
||||||
|
|
||||||
for (x, y) in edges {
|
for (x, y) in edges {
|
||||||
assert!(graph.add_edge(x, y, nop));
|
assert!(graph.add_edge(x, y, nop).is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/lib.rs
41
src/lib.rs
@@ -139,19 +139,28 @@ impl MutexId {
|
|||||||
///
|
///
|
||||||
/// This method panics if the new dependency would introduce a cycle.
|
/// This method panics if the new dependency would introduce a cycle.
|
||||||
pub fn mark_held(&self) {
|
pub fn mark_held(&self) {
|
||||||
let creates_cycle = HELD_LOCKS.with(|locks| {
|
let opt_cycle = HELD_LOCKS.with(|locks| {
|
||||||
if let Some(&previous) = locks.borrow().last() {
|
if let Some(&previous) = locks.borrow().last() {
|
||||||
let mut graph = get_dependency_graph();
|
let mut graph = get_dependency_graph();
|
||||||
|
|
||||||
!graph.add_edge(previous, self.value(), MutexDep::capture)
|
graph
|
||||||
|
.add_edge(previous, self.value(), MutexDep::capture)
|
||||||
|
.err()
|
||||||
} else {
|
} else {
|
||||||
false
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if creates_cycle {
|
if let Some(cycle) = opt_cycle {
|
||||||
// Panic without holding the lock to avoid needlessly poisoning it
|
// Panic without holding the lock to avoid needlessly poisoning it
|
||||||
panic!("Mutex order graph should not have cycles");
|
let mut message = String::from("Found cycle in mutex dependency graph:\n");
|
||||||
|
|
||||||
|
for entry in cycle {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let _ = writeln!(message, "{entry}");
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("{message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
HELD_LOCKS.with(|locks| locks.borrow_mut().push(self.value()));
|
HELD_LOCKS.with(|locks| locks.borrow_mut().push(self.value()));
|
||||||
@@ -270,6 +279,12 @@ impl MutexDep {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MutexDep {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a reference to the current dependency graph
|
/// Get a reference to the current dependency graph
|
||||||
fn get_dependency_graph() -> impl DerefMut<Target = DiGraph<usize, MutexDep>> {
|
fn get_dependency_graph() -> impl DerefMut<Target = DiGraph<usize, MutexDep>> {
|
||||||
static DEPENDENCY_GRAPH: OnceLock<Mutex<DiGraph<usize, MutexDep>>> = OnceLock::new();
|
static DEPENDENCY_GRAPH: OnceLock<Mutex<DiGraph<usize, MutexDep>>> = OnceLock::new();
|
||||||
@@ -303,11 +318,17 @@ mod tests {
|
|||||||
let c = LazyMutexId::new();
|
let c = LazyMutexId::new();
|
||||||
|
|
||||||
let mut graph = get_dependency_graph();
|
let mut graph = get_dependency_graph();
|
||||||
assert!(graph.add_edge(a.value(), b.value(), MutexDep::capture));
|
assert!(graph
|
||||||
assert!(graph.add_edge(b.value(), c.value(), MutexDep::capture));
|
.add_edge(a.value(), b.value(), MutexDep::capture)
|
||||||
|
.is_ok());
|
||||||
|
assert!(graph
|
||||||
|
.add_edge(b.value(), c.value(), MutexDep::capture)
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
// Creating an edge c → a should fail as it introduces a cycle.
|
// Creating an edge c → a should fail as it introduces a cycle.
|
||||||
assert!(!graph.add_edge(c.value(), a.value(), MutexDep::capture));
|
assert!(graph
|
||||||
|
.add_edge(c.value(), a.value(), MutexDep::capture)
|
||||||
|
.is_err());
|
||||||
|
|
||||||
// Drop graph handle so we can drop vertices without deadlocking
|
// Drop graph handle so we can drop vertices without deadlocking
|
||||||
drop(graph);
|
drop(graph);
|
||||||
@@ -315,7 +336,9 @@ mod tests {
|
|||||||
drop(b);
|
drop(b);
|
||||||
|
|
||||||
// If b's destructor correctly ran correctly we can now add an edge from c to a.
|
// If b's destructor correctly ran correctly we can now add an edge from c to a.
|
||||||
assert!(get_dependency_graph().add_edge(c.value(), a.value(), MutexDep::capture));
|
assert!(get_dependency_graph()
|
||||||
|
.add_edge(c.value(), a.value(), MutexDep::capture)
|
||||||
|
.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test creating a cycle, then panicking.
|
/// Test creating a cycle, then panicking.
|
||||||
|
|||||||
@@ -696,7 +696,7 @@ pub mod tracing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "Mutex order graph should not have cycles")]
|
#[should_panic(expected = "Found cycle in mutex dependency graph")]
|
||||||
fn test_detect_cycle() {
|
fn test_detect_cycle() {
|
||||||
let a = Mutex::new(());
|
let a = Mutex::new(());
|
||||||
let b = Mutex::new(());
|
let b = Mutex::new(());
|
||||||
|
|||||||
Reference in New Issue
Block a user