{"record":{"id":"d35bab318b759405","repo":"Hmbown/CodeWhale","slug":"catalog-cache-unavailable","errorCode":null,"errorMessage":"catalog cache unavailable","messagePattern":"catalog cache unavailable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/provider_catalog_live.rs","lineNumber":684,"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":666,"sourceCodeEnd":702,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/provider_catalog_live.rs#L666-L702","documentation":"`ensure_cache_loaded` lazily loads the on-disk provider-catalog cache into a process-global `CACHE` behind a `RwLock`. If acquiring the write lock fails, the RwLock is poisoned — meaning a thread panicked while holding it — and the library surfaces the opaque 'catalog cache unavailable' error rather than risk reading inconsistent cache state.","triggerScenarios":"`cached_entry_for_route` (via `ensure_cache_loaded`) is called after some earlier thread panicked while mutating `CACHE` (e.g. a bug in cache deserialization/persist code), leaving the RwLock poisoned for the rest of the process.","commonSituations":"A latent panic in cache-loading code runs once early in the session; every later catalog lookup then fails with this error even though the disk file is fine; usually seen only after another suppressed panic.","solutions":["Restart the process — RwLock poisoning is process-global and clears only on restart.","Reproduce the original panic (it appears earlier in logs) and fix that root-cause bug; this error is only the echo.","Check the catalog cache file for malformed content that could make the loading code panic."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match ensure_cache_loaded() {\n    Err(e) if e.to_string().contains(\"catalog cache unavailable\") => {\n        // poisoned lock: skip cache, go straight to network refresh, log for restart\n        log::warn(\"catalog cache poisoned; bypassing until restart\");\n        refresh_from_network()\n    }\n    r => r,\n}","preventionTips":["Investigate any earlier panic in cache-loading code — it is the real bug","Monitor logs for the first panic; this error always echoes it","Restart long-lived processes after any catalog-cache panic"],"tags":["concurrency","cache","mutex-poisoned","internal"],"backgroundTag":"internal-invariant-violation","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}