Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 own OnceLock. The first logger successfully installed receives later events. An earlier no_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 later start() 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:

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.