{"record":{"id":"d35bab318b759405","repo":"Hmbown/CodeWhale","slug":"catalog-cache-unavailable","errorCode":null,"errorMessage":"catalog cache unavailable","messagePattern":"catalog cache unavailable","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/provider_catalog_live.rs","lineNumber":677,"sourceCode":"\nfn load_from_disk() -> Option<ProviderCatalogCache> {\n    let path = cache_path()?;\n    if !path.is_file() {\n        return None;\n    }\n    let lock_file = open_cache_lock(&cache_lock_path(&path)).ok()?;\n    let lock = fd_lock::RwLock::new(lock_file);\n    let _guard = lock.read().ok()?;\n    load_from_disk_unlocked(&path)\n}\n\nfn ensure_cache_loaded() -> Result<()> {\n    if DISK_LOADED.load(Ordering::Acquire) {\n        return Ok(());\n    }\n    let mut cache = CACHE\n        .write()\n        .map_err(|_| anyhow::anyhow!(\"catalog cache unavailable\"))?;\n    if DISK_LOADED.load(Ordering::Acquire) {\n        return Ok(());\n    }\n    if let Some(path) = cache_path() {\n        match fs::symlink_metadata(&path) {\n            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n            Err(err) => return Err(err.into()),\n            Ok(_) => {\n                let loaded = load_from_disk().context(\"invalid provider catalog cache\")?;\n                for (key, entry) in loaded.entries {\n                    if cache\n                        .entries\n                        .get(&key)\n                        .is_none_or(|local| entry.fetched_at >= local.fetched_at)\n                    {\n                        cache.entries.insert(key, entry);\n                    }\n                }","sourceCodeStart":659,"sourceCodeEnd":695,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/provider_catalog_live.rs#L659-L695","documentation":"ensure_cache_loaded takes a write lock on the in-memory catalog CACHE (a RwLock) while first loading from disk. If the lock is poisoned — a previous thread panicked while holding it — lock().write() returns Err and this error is thrown instead of proceeding.","triggerScenarios":"Calling cached_entry_for_route (which calls ensure_cache_loaded) after any panic occurred in another thread while mutating the CACHE map, e.g. during disk load or scope writes.","commonSituations":"After a panic elsewhere in catalog code; long-running TUI sessions that hit an earlier bug and now permanently fail catalog cache reads.","solutions":["Fix the underlying panic that poisoned the CACHE lock (check earlier panics in logs)","Restart the TUI process to get a fresh, unpoisoned lock","Wrap cache mutations in catch_unwind or restructure so no code panics while holding the lock","If persistent, delete the on-disk catalog cache to rule out malformed-data-driven panics"],"exampleFix":"// before\nlet mut cache = CACHE.write().map_err(|_| anyhow::anyhow!(\"catalog cache unavailable\"))?;\n// after\nlet mut cache = CACHE.write().unwrap_or_else(|e| e.into_inner()); // recover from poison","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match ensure_cache_loaded() {\n    Err(e) if e.to_string().contains(\"catalog cache unavailable\") => {\n        // lock poisoned: restart session or fall back to network refresh\n        return fallback_to_live_catalog();\n    }\n    Ok(()) => {},\n    Err(e) => return Err(e),\n}","preventionTips":["Avoid panic! while holding catalog cache locks; return Result instead","Recover poison with unwrap_or_else(|e| e.into_inner()) in this module","Watch logs for earlier catalog panics — poison is always secondary","Restart the TUI after any catalog-related panic"],"tags":["rust","concurrency","mutex-poisoned","cache"],"backgroundTag":"internal-invariant-violation","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}