Manage Lifecycle and Callbacks
Initialize Deloxide once, at the process boundary, before instrumented threads begin work and before the locks whose behavior you want to diagnose are created. Configuration is process-wide; it is not a per-request, per-test-case, or per-worker service.
#![allow(unused)]
fn main() {
extern crate deloxide;
use deloxide::{DeadlockSource, Deloxide};
Deloxide::new()
.callback(|report| match report.source {
DeadlockSource::WaitForGraph => {
eprintln!("active tracked deadlock: {:?}", report.thread_cycle);
}
DeadlockSource::LockOrderViolation => {
eprintln!("potential lock-order cycle: {:?}", report.lock_order_cycle);
}
})
.start()
.expect("Deloxide must start before the workload");
// Construct tracked locks and start worker/request processing after this point.
}
WaitForGraph means an active, validated cycle among the detector’s currently
tracked waits and incompatible owners. LockOrderViolation, available with the
optional order-graph feature, is a potential ordering risk rather than an active
deadlock. See Reading a Deadlock Report for the
triage workflow and the exact Deloxide
and DeadlockSource
APIs.
One detector, partial repeated-start behavior
Deloxide keeps a global detector for the process. Calling
Deloxide::start
does not create an isolated detector or reject a call merely because one start
already completed. A later call still runs initialization and, assuming any
requested logger can be constructed, normally returns Ok(()). Its effects are
deliberately not an all-or-nothing reconfiguration:
- The callback uses a
OnceLock; the first callback successfully installed in the process handles later reports. A later builder’s callback is not installed. - With
logging-and-visualization, the global logger has its ownOnceLock. The first logger successfully installed receives later events. An earlierno_logging()start leaves that slot empty, so a later start can install the first logger. Once installed, a later log path does not replace it, although that laterstart()still attempts to create its configured logger before the one-time install and can return an I/O error. - With
lock-order-graph, a later start with checking enabled creates or replaces the detector’s order graph with a new graph. A later start with checking disabled does not remove an order graph that already exists. - With
stress-test, every start overwrites the process-wide stress mode and stress configuration, including overwriting them with the builder defaults.
Existing ownership, wait, and other detector state is not cleared as one coherent reset while those feature-specific fields change. Repeated starts are therefore partial, unsupported reconfiguration, not a reliable reset or runtime toggle. Initialize once before instrumented work and use a separate process when a clean configuration or detector state is required. There is deliberately no public shutdown, reset, or reconfigure API in this guidance.
This matters in tests. Put cases requiring different Deloxide configurations in
separate test processes (for example, separate integration-test binaries or
separate cargo test --test name invocations), rather than parallel tests in one
process. A test that installs the default callback can affect every later test in
that binary.
The default callback panics with the report, but callback execution is isolated; choose an explicit callback for application policy instead of assuming that default behavior terminates the process.
Use tracked thread entry points
deloxide::thread
re-exports common std::thread items such as JoinHandle, current, sleep,
park, and yield_now. It provides tracked versions of:
thread::spawn,thread::Builderwithspawn, andBuilder::spawn_scoped.
These helpers register thread creation and exit. On creation they retain the parent’s Deloxide thread ID; with logging enabled, that parent/child information is emitted with the thread-spawn event so a log can relate a worker to its creator.
#![allow(unused)]
fn main() {
extern crate deloxide;
use deloxide::thread;
let named = thread::Builder::new()
.name("indexer".to_owned())
.spawn(|| 42)
.expect("worker creation");
assert_eq!(named.join().expect("worker result"), 42);
let value = 0;
thread::scope(|scope| {
// `scope.spawn` is std's scoped spawn; use the tracked Builder helper here.
thread::Builder::new()
.spawn_scoped(scope, || assert_eq!(value, 0))
.expect("scoped worker creation")
.join()
.expect("scoped worker result");
});
}
thread::scope
provides the standard scoped-thread boundary, but its Scope is the standard
library type. Therefore scope.spawn(...) is an ordinary scoped spawn; use
thread::Builder::spawn_scoped(scope, ...) when creation/exit tracking matters.
Ordinary std::thread::spawn does not make the detector blind to every operation
inside that thread: a standard thread that locks a Deloxide Mutex, RwLock, or
uses a compatible Deloxide Condvar still runs the wrapper code, so supported
lock waits and acquisitions can be observed. What it lacks is the tracked
thread’s spawn/exit registration and parent/child log relationship. Ordinary
threads also do not make std::sync or parking_lot locks observable; migrate
those lock instances separately.
Keep callbacks an alert handoff
Deloxide queues callbacks to one background dispatcher thread rather than running them on the thread that detected the finding. A panic from one callback invocation is caught, reported to stderr, and does not stop that dispatcher from handling a later report. That isolation is useful, but it is not permission to do recovery work in the callback: callbacks are serialized, and a callback can still block on an application lock, do slow I/O, or delay every subsequent report.
Keep the handler bounded and nonblocking. Copy or move the
DeadlockInfo
into a bounded queue with try_send, count overload, and let a separate
supervisor persist, page, or capture diagnostics.
#![allow(unused)]
fn main() {
extern crate deloxide;
use deloxide::{DeadlockInfo, Deloxide};
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
mpsc,
};
let (reports_tx, reports_rx) = mpsc::sync_channel::<DeadlockInfo>(64);
let dropped = Arc::new(AtomicU64::new(0));
let callback_dropped = Arc::clone(&dropped);
Deloxide::new()
.callback(move |report| {
if reports_tx.try_send(report).is_err() {
callback_dropped.fetch_add(1, Ordering::Relaxed);
}
})
.start()
.expect("detector initialization");
let _supervisor_inputs = (reports_rx, dropped);
}
The process may exit, abort, or be terminated before a queued callback, a log write, or the supervising task completes. Do not make process termination from a callback your evidence-preservation strategy; persist or export what you need on the normal incident path, and test that path independently.