Show cycle backtraces when they happen

This commit is contained in:
2023-08-27 18:06:11 +02:00
parent 6be3e05cab
commit 068303d81d
5 changed files with 96 additions and 33 deletions

View File

@@ -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
View 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
}

View File

@@ -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());
} }
} }
} }

View File

@@ -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.

View File

@@ -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(());