Hmbown/CodeWhale · error

active catalog write lock

Error message

active catalog write lock

What it means

`ActiveCatalogGuard::drop` calls `.write().expect("active catalog write lock")` on the global active-catalog RwLock. The panic fires only if the lock is poisoned, i.e. another thread panicked while holding the catalog lock and left it in an inconsistent state. This is a deliberate fail-fast: once the shared catalog invariant is broken the test harness must not silently continue with a corrupt catalog.

Solutions

  1. Fix the original panic that poisoned the lock — the poisoned-lock expect is a secondary failure
  2. Run tests single-threaded (`cargo test -- --test-threads=1`) to confirm which test poisons the catalog lock
  3. Scope catalog replacement so a panic while the lock is held releases it cleanly (avoid panicking between lock acquisition and guard drop)
  4. If recovery is desired, replace `.expect()` with `unwrap_or_else(PoisonError::into_inner)` to take the lock despite poisoning

Example fix

// before
let mut active = active_catalog().write().expect("active catalog write lock");
// after
let mut active = active_catalog().write().unwrap_or_else(PoisonError::into_inner);
Defensive patterns

Strategy: try-catch

Validate before calling

// Best-effort pre-check in tests: detect poisoning before swapping
fn catalog_lock_healthy() -> bool {
    std::panic::catch_unwind(|| active_catalog().read().is_ok()).unwrap_or(false)
}

Try / catch

let mut active = active_catalog().write()
    .unwrap_or_else(PoisonError::into_inner); // recover instead of panic
// or, to surface the original panic:
// match active_catalog().write() { Ok(g) => g, Err(poisoned) => panic!("catalog lock poisoned: {poisoned}") }

Prevention

When it happens

Trigger: Dropping an `ActiveCatalogGuard` returned by `replace_active_catalog_for_test` while the active-catalog RwLock is poisoned — typically because another test (or the guard itself) panicked while holding the lock earlier in the same process.

Common situations: Parallel test threads sharing the global catalog; a test that panics inside a closure holding the catalog read/write guard; a `replace_active_catalog_for_test` guard leaked across a panic boundary so the poisoned lock is re-locked on unwind.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/41fd3f1cc3accc23. Report an issue: GitHub.

Appendix: source

Thrown at crates/models/src/model_catalog.rs:217

#[cfg(any(test, feature = "test-support"))]
static TEST_CATALOG_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
    std::sync::LazyLock::new(|| std::sync::Mutex::new(()));

#[cfg(any(test, feature = "test-support"))]
pub fn test_catalog_lock() -> std::sync::MutexGuard<'static, ()> {
    TEST_CATALOG_LOCK.lock().expect("model catalog test lock")
}

#[cfg(any(test, feature = "test-support"))]
pub struct ActiveCatalogGuard {
    previous: MergedCatalog,
}

#[cfg(any(test, feature = "test-support"))]
impl Drop for ActiveCatalogGuard {
    fn drop(&mut self) {
        let mut active = active_catalog().write().expect("active catalog write lock");
        *active = self.previous.clone();
    }
}

#[cfg(any(test, feature = "test-support"))]
pub fn replace_active_catalog_for_test(catalog: MergedCatalog) -> ActiveCatalogGuard {
    let mut active = active_catalog().write().expect("active catalog write lock");
    let previous = active.clone();
    *active = catalog;
    ActiveCatalogGuard { previous }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn entry(id: &str, context_window: u32, provenance: MetadataProvenance) -> CatalogEntry {
        CatalogEntry {

View on GitHub (pinned to 433685b202)