{"record":{"id":"41fd3f1cc3accc23","repo":"Hmbown/CodeWhale","slug":"active-catalog-write-lock","errorCode":null,"errorMessage":"active catalog write lock","messagePattern":"active catalog write lock","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/models/src/model_catalog.rs","lineNumber":217,"sourceCode":"\n#[cfg(any(test, feature = \"test-support\"))]\nstatic TEST_CATALOG_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =\n    std::sync::LazyLock::new(|| std::sync::Mutex::new(()));\n\n#[cfg(any(test, feature = \"test-support\"))]\npub fn test_catalog_lock() -> std::sync::MutexGuard<'static, ()> {\n    TEST_CATALOG_LOCK.lock().expect(\"model catalog test lock\")\n}\n\n#[cfg(any(test, feature = \"test-support\"))]\npub struct ActiveCatalogGuard {\n    previous: MergedCatalog,\n}\n\n#[cfg(any(test, feature = \"test-support\"))]\nimpl Drop for ActiveCatalogGuard {\n    fn drop(&mut self) {\n        let mut active = active_catalog().write().expect(\"active catalog write lock\");\n        *active = self.previous.clone();\n    }\n}\n\n#[cfg(any(test, feature = \"test-support\"))]\npub fn replace_active_catalog_for_test(catalog: MergedCatalog) -> ActiveCatalogGuard {\n    let mut active = active_catalog().write().expect(\"active catalog write lock\");\n    let previous = active.clone();\n    *active = catalog;\n    ActiveCatalogGuard { previous }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn entry(id: &str, context_window: u32, provenance: MetadataProvenance) -> CatalogEntry {\n        CatalogEntry {","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/models/src/model_catalog.rs#L199-L235","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Fix the original panic that poisoned the lock — the poisoned-lock expect is a secondary failure","Run tests single-threaded (`cargo test -- --test-threads=1`) to confirm which test poisons the catalog lock","Scope catalog replacement so a panic while the lock is held releases it cleanly (avoid panicking between lock acquisition and guard drop)","If recovery is desired, replace `.expect()` with `unwrap_or_else(PoisonError::into_inner)` to take the lock despite poisoning"],"exampleFix":"// before\nlet mut active = active_catalog().write().expect(\"active catalog write lock\");\n// after\nlet mut active = active_catalog().write().unwrap_or_else(PoisonError::into_inner);","handlingStrategy":"try-catch","validationCode":"// Best-effort pre-check in tests: detect poisoning before swapping\nfn catalog_lock_healthy() -> bool {\n    std::panic::catch_unwind(|| active_catalog().read().is_ok()).unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"let mut active = active_catalog().write()\n    .unwrap_or_else(PoisonError::into_inner); // recover instead of panic\n// or, to surface the original panic:\n// match active_catalog().write() { Ok(g) => g, Err(poisoned) => panic!(\"catalog lock poisoned: {poisoned}\") }","preventionTips":["Never panic while holding the active-catalog lock; clone data out, release, then assert","Scope `replace_active_catalog_for_test` guards tightly so unwinds release them predictably","Run catalog-touching tests with a serial group to avoid cross-test poisoning"],"tags":["rust","lock","test-support","poisoning"],"backgroundTag":"lock-poisoned","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}