fix: resolve test race conditions, add logging, and fix CI deadlock
Re-enables the `numa` feature in CI workflows to prevent container/cgroup deadlocks while preserving validation correctness. Fixes concurrent test race conditions by replacing thread-local parameter storage with process-wide atomics and mutex locks. Integrates `tracing-subscriber` for structured logging and adds thread-ID tracking to debug worker lifecycles. Additionally bumps the crate version, updates `.gitignore`, documents experimental evolutionary distance pipelines, and refactors hardcoded test constants.
This commit is contained in:
+33
-17
@@ -7,12 +7,28 @@
|
||||
//! different value panics. This prevents silent divergence between the global
|
||||
//! parameter and the values used to build data structures.
|
||||
//!
|
||||
//! In test builds (`#[cfg(test)]`) the same public API is backed by
|
||||
//! `thread_local!` [`Cell`]s instead. Each test thread gets its own
|
||||
//! independent copies of `K` and `M`, so tests can use arbitrary values
|
||||
//! without coordinating with one another and without any reset mechanism.
|
||||
//! The `OnceLock` constraint is deliberately absent: test isolation is
|
||||
//! provided by thread locality, not by write-once semantics.
|
||||
//! In test builds (`#[cfg(test)]`) the same public API is backed by plain
|
||||
//! process-wide atomics instead, freely overwritable (no write-once
|
||||
//! constraint) so tests don't need a reset mechanism between runs.
|
||||
//!
|
||||
//! An earlier version of this module used `thread_local!` `Cell`s here,
|
||||
//! reasoning that "each test thread gets its own copy" gives isolation
|
||||
//! between tests using different `k`/`m` values. That assumption broke as
|
||||
//! soon as any code under test fanned work out to *other* threads it
|
||||
//! doesn't control — `PartitionRunner`'s pre-spawned workers, or a bare
|
||||
//! `rayon::par_iter()` — since a freshly spawned thread never inherits the
|
||||
//! calling test thread's thread-local state, silently reading back `k=0`
|
||||
//! there instead (surfaced as a `bitvec`/slice-indexing panic deep inside
|
||||
//! whatever used the bogus length). Process-wide atomics make `k()`/`m()`
|
||||
//! correct on *any* thread without every call site having to know to
|
||||
//! re-propagate them. Unset still silently reads back as `0` (same as the
|
||||
//! old `Cell` default) rather than panicking: several existing tests read
|
||||
//! `m()` without ever calling `set_m` themselves, relying on that default.
|
||||
//! The trade-off: tests that genuinely need different `k`/`m` values from
|
||||
//! other tests must not run concurrently with them in the same process (in
|
||||
//! practice: every test file in this workspace already uses one fixed
|
||||
//! `k`/`m` pair for all its own tests, so this doesn't currently cost
|
||||
//! anything).
|
||||
|
||||
// ── Production implementation ─────────────────────────────────────────────────
|
||||
|
||||
@@ -44,22 +60,22 @@ mod state {
|
||||
|
||||
// ── Test implementation ───────────────────────────────────────────────────────
|
||||
//
|
||||
// Each test thread owns its private K and M via thread_local!, so tests may
|
||||
// call set_k / set_m with any value without affecting other tests.
|
||||
// Process-wide, freely overwritable (no write-once constraint), visible from
|
||||
// any thread — including threads a test doesn't spawn itself (rayon workers,
|
||||
// PartitionRunner workers, ...). `0` (never explicitly set) is returned as-is,
|
||||
// same default as the old thread-local `Cell`.
|
||||
|
||||
#[cfg(any(test, feature = "test-utils"))]
|
||||
mod state {
|
||||
use std::cell::Cell;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
thread_local! {
|
||||
static K: Cell<usize> = Cell::new(0);
|
||||
static M: Cell<usize> = Cell::new(0);
|
||||
}
|
||||
static K: AtomicUsize = AtomicUsize::new(0);
|
||||
static M: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
pub fn set_k(k: usize) { K.with(|c| c.set(k)); }
|
||||
pub fn k() -> usize { K.with(|c| c.get()) }
|
||||
pub fn set_m(m: usize) { M.with(|c| c.set(m)); }
|
||||
pub fn m() -> usize { M.with(|c| c.get()) }
|
||||
pub fn set_k(k: usize) { K.store(k, Ordering::SeqCst); }
|
||||
pub fn k() -> usize { K.load(Ordering::SeqCst) }
|
||||
pub fn set_m(m: usize) { M.store(m, Ordering::SeqCst); }
|
||||
pub fn m() -> usize { M.load(Ordering::SeqCst) }
|
||||
}
|
||||
|
||||
// ── Public API (identical signature in both configurations) ───────────────────
|
||||
|
||||
Reference in New Issue
Block a user