{"record":{"id":"bdde038b125195b6","repo":"Hmbown/CodeWhale","slug":"clock-lock-failed","errorCode":null,"errorMessage":"Clock lock failed","messagePattern":"Clock lock failed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/tui/src/tui/pet_watch/owner.rs","lineNumber":463,"sourceCode":"        .map_err(|_| anyhow::anyhow!(\"Shared habitat could not restore; its file was kept\"))?;\n    let mut producer: Option<(String, u64, Instant, String)> = None;\n    let mut audio: Option<(String, Instant)> = None;\n    let mut playback: Option<(super::audio::Output, super::audio_cursor::AudioCursor)> = None;\n    let mut audio_error = false;\n    let mut waiting = false;\n    let mut last_save = Instant::now();\n    let mut storage_error = false;\n    let origin = Instant::now();\n    let initial_time: f64 = context.with(|ctx| ctx.eval(\"JSON.parse(pet.snapshot()).timeMs\"))?;\n    let mut last = origin;\n    let mut ticks = 0u64;\n    let mut measurements = VecDeque::<f64>::new();\n    save(&context, &mut saved, &mut store)?;\n    let _ = ready.send(Ok(()));\n    loop {\n        *deadline\n            .lock()\n            .map_err(|_| anyhow::anyhow!(\"Clock lock failed\"))? =\n            Instant::now() + Duration::from_secs(5);\n        let now = Instant::now();\n        if !same_file(&lock_path.open_update(false, false)?, original)? {\n            anyhow::bail!(\"Owner lock was replaced\");\n        }\n        if producer\n            .as_ref()\n            .is_some_and(|(_, _, seen, _)| now.duration_since(*seen) > LEASE)\n        {\n            producer = None;\n            waiting = false;\n            context.with(|ctx| ctx.eval::<(), _>(\"pet.disconnectEngine()\"))?;\n        }\n        if audio\n            .as_ref()\n            .is_some_and(|(_, seen)| now.duration_since(*seen) > LEASE)\n        {\n            audio = None;","sourceCodeStart":445,"sourceCodeEnd":481,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/tui/pet_watch/owner.rs#L445-L481","documentation":"The pet owner loop keeps a JS interrupt deadline under a `Mutex<Instant>`; before each tick it locks that mutex and resets the deadline five seconds out so the QuickJS interrupt handler sees fresh work. This error is raised only when `Mutex::lock` itself fails — i.e. the mutex was poisoned because another thread panicked while holding the lock. The deadline mutex is shared with the interrupt-handler closure installed at runtime creation, so a poisoned lock means the interrupt machinery is no longer trustworthy and the loop aborts rather than ticking with an unbounded JS budget.","triggerScenarios":"Any thread or closure that panics while holding the `deadline` mutex — in practice the interrupt-handler closure (`limit.lock().map_or(true, |d| Instant::now() > *d)`) panicking inside QuickJS, or any other holder panicking between lock and release — after which every subsequent `deadline.lock()` in the tick loop returns `PoisonError` and maps to this error.","commonSituations":"A panic inside the QuickJS interrupt handler during script evaluation (e.g. an unwinding bug in a native callback) poisoning the mutex on a long-running pet owner; this is essentially never a user-config problem — it indicates an internal panic earlier in the same `pet serve` process, whose message would have appeared first.","solutions":["Look earlier in the stderr/stdout of `pet serve` for the original panic message — poisoning is a downstream symptom; fix or report that panic.","Restart `pet serve`; the mutex poisoning does not persist across processes, so a fresh start clears it.","If reproducible, capture the panic backtrace (RUST_BACKTRACE=1 pet serve ...) and file a bug against the code path that panicked while holding the deadline lock.","As a defensive change, wrap deadline updates in `lock().unwrap_or_else(|p| p.into_inner())` if a stale deadline is acceptable — though upstream intentionally treats poisoning as fatal here."],"exampleFix":"// before: poisoning propagates as an opaque error\n*deadline.lock().map_err(|_| anyhow::anyhow!(\"Clock lock failed\"))? = Instant::now() + Duration::from_secs(5);\n// after: recover from poisoning (deadline is a plain Instant, safe to reuse)\n*deadline.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) = Instant::now() + Duration::from_secs(5);","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Poisoning means some holder panicked; recover only when the guarded data is trivially valid:\nmatch deadline.lock() {\n    Ok(mut d) => *d = Instant::now() + Duration::from_secs(5),\n    Err(poisoned) => {\n        let mut d = poisoned.into_inner(); // Instant is always valid\n        *d = Instant::now() + Duration::from_secs(5);\n        log::warn!(\"deadline mutex was poisoned; recovered\");\n    }\n}","preventionTips":["Keep the deadline-mutex critical sections panic-free (no unwrap/indexing inside the lock).","Set RUST_BACKTRACE=1 when running `pet serve` so the original poisoning panic is diagnosable.","For trivially-valid data like `Instant`, recover with `into_inner()` instead of failing the tick loop.","Treat this error as a symptom: hunt the earlier panic, not the lock failure."],"tags":["mutex","poisoned-lock","panic","concurrency","quickjs"],"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-22T06:17:15.046Z"}