From 18b680c9aa8233389da33bf7b33226922e65ecef Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 15 Oct 2026 01:26:06 -0400 Subject: [PATCH] Recovery: a record lock held for a moment is waited out, not skipped new_draft_restarts_restores_unsaved_and_explicitly_discards failed in a workspace run: the reopened session found no recovery record, because a process another test was starting held the record's lock for a moment (a forked child shares it until exec) and scan() skips locked records. The same can happen to a user when the app starts a helper process. lock() now tries again up to ten times, 10 ms apart, before treating the record as owned by a live instance. Co-authored-by: Claude Opus 5.5 --- crates/engine/src/recovery/desktop.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/engine/src/recovery/desktop.rs b/crates/engine/src/recovery/desktop.rs index 1480363ae..676a6ed0c 100644 --- a/crates/engine/src/recovery/desktop.rs +++ b/crates/engine/src/recovery/desktop.rs @@ -14,6 +14,8 @@ const MAX_BYTES: u64 = 256 << 20; const MAX_RECORDS: usize = 128; const MAX_FILES: usize = 16; const DISK_BUDGET: u64 = 2 << 30; +/// Times a record lock held for a moment (by a process being started) is tried again. +const LOCK_RETRIES: u32 = 10; #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -127,11 +129,21 @@ impl Store { paths.sort_by_key(|p| std::cmp::Reverse(generation(p))); Ok(paths) } + /// Locks a record's `owner.lock`, retrying [`LOCK_RETRIES`] times 10 ms apart. fn lock(&self, id: &str) -> Result { if !id_ok(id) { return Err("invalid recovery id".into()); } let file = private_file(&self.root.join(id).join("owner.lock"), false).map_err(|e| e.to_string())?; + // A process another part of the app (or a test) is starting holds the lock for a moment + // (a forked child shares it until it execs): wait that out, but not a live owner. + for _ in 0..LOCK_RETRIES { + match file.try_lock() { + Ok(()) => return Ok(file), + Err(std::fs::TryLockError::WouldBlock) => std::thread::sleep(Duration::from_millis(10)), + Err(e) => return Err(format!("recovery copy is in use or cannot be locked: {e}")), + } + } file.try_lock().map_err(|e| format!("recovery copy is in use or cannot be locked: {e}"))?; Ok(file) }