fix(packages,daemon): reclaim dead-owner install locks; stop leaving stale endpoint records (#1213) - #1227
Conversation
… 1+3) ## Part 1 — the deadlock `owner.txt` has been recording the holder's `pid=` all along; `lock_is_stale` just never read it. It checked only the directory mtime against a 2 h ceiling, so a crashed install wedged every later build for up to two hours — the reporter measured ~40 minutes of `waiting for another process to install ...` with no fbuild process alive. `lock_is_stale` now reclaims a lock when the owning process is gone, which is the fix the issue asked for and needed no new bookkeeping to implement. Three ordered reasons to reclaim, and the ordering is deliberate: 1. the directory vanished; 2. the owner is dead (new); 3. the age ceiling (unchanged backstop, for what liveness can't answer — a missing owner record on a foreign filesystem, or a hung-but-running peer). Care taken not to reclaim a *live* install: - PID recycling is handled by also recording `exe_stem=` and comparing it. A recycled PID now running some other program means the original owner is gone; an fbuild PID that is genuinely alive keeps its lock. - Every probe fails safe. `pid_executable_path` returning `None` (permission denied, race) is treated as alive, never as dead. - Records written before `exe_stem` existed fall back to liveness alone. - `create_dir` and `write_lock_owner` are two steps, so a waiter can observe the gap. A lock with no owner record is left alone for a 30 s grace period, then reclaimed — otherwise a crash *between* those two calls still wedged for the full 2 h. ## Part 3 — undiagnosable esptool failure "cached esptool executable ... exited with status 2" carried no output, which is what the reporter hit on Windows with a freshly-installed 5.3.0. The error now includes captured stderr/stdout, and says so explicitly when both are empty rather than leaving the reader guessing. 7 new tests: dead PID reclaimed immediately, live PID never reclaimed, recycled PID treated as dead, legacy record without exe_stem, the create/write race window left alone, an abandoned-mid-creation lock reclaimed after grace, and the owner record round-trip. Part 2 (stale daemon endpoint surviving `daemon stop`) is not addressed here; it is daemon-lifecycle work with a different blast radius. Co-Authored-By: Claude <noreply@anthropic.com>
…ness (#1213 part 2) Implements both fixes the issue suggests, plus one omission found on the way. - `daemon stop` now clears the port/pid/status/owner-claim records when it finds no daemon. The early return was the bug: "not running" is precisely when those records are known to be garbage, yet it was the one path that left them untouched. It also sweeps after a successful shutdown, since the daemon does not remove all of them itself. - The daemon's graceful shutdown removed pid, port and owner claim but NOT `daemon_status.json`, so `daemon status` kept describing a dead PID after a *clean* exit, not just a crash. Now removed too. - `get_daemon_port()` no longer trusts the port file verbatim: it is ignored when the recorded owner is provably gone. Fails safe in every uncertain case — a missing claim (including the startup window where the port file exists but the claim does not yet) counts as alive, and an uninspectable process counts as alive. PID recycling is covered by the exe-stem check. Scope, stated plainly: this does NOT explain the reporter's "clients keep dialing a dead port". The port is derived deterministically from (version, cache identity) (#1009), so discarding the port file yields the same number — which is exactly why deleting the files by hand didn't help them either. That symptom is a daemon-respawn failure, and diagnosing it needs a repro I don't have. What this fixes is the record hygiene the issue asks for, so the state stops lying about a dead daemon. Co-Authored-By: Claude <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 56 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
kill(0, 0) probes the caller's own process group and succeeds, so the unix liveness check reported pid 0 as alive. That failed the two #1227 install-lock staleness tests on macOS/Linux (Check macOS red on PR #1229; Ubuntu masked behind the clippy error), and in production a corrupt owner record holding pid=0 would never be reclaimed — the exact #1213 deadlock class. Guard pid 0 explicitly and add a cross-platform regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…est (#1228) (#1232) The #1228 trace on current main shows the residual respawn failure no longer reproduces: after an unclean daemon kill, the next client invocation detects the dead endpoint (via the #1227 owner-liveness gate), respawns a sibling fbuild-daemon on the same deterministic port, and the request succeeds (~5s observed). Pin that behavior end-to-end: spawn the real binaries, terminate the daemon hard, and assert the very next CLI invocation reaches a NEW daemon PID. Ignore-gated like the #1159 real-daemon test; skips rather than contending when a live dev daemon owns the real ~/.fbuild/dev root, since the production spawn path rebuilds the daemon env from the OS user baseline and cannot be HOME-isolated. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Addresses #1213 parts 1, 2 and 3.
Part 1 — the deadlock (the one that cost 40 minutes)
The headline finding:
owner.txtwas already recording the holder'spid=.lock_is_stalejust never read it. It checked only the directory mtime against a 2 h ceiling, so a crashed install wedged every later build for up to two hours — matching the reported ~40 min ofwaiting for another process to install ...with no fbuild process alive. The data needed to fix this was already on disk.lock_is_stalenow reclaims a lock when its owner is gone. Three ordered reasons, deliberately ordered by confidence:Because the failure mode of getting this wrong is deleting a live install's lock, every probe fails safe toward "alive":
exe_stem=. A recycled PID now running some other program means the original owner is gone; a genuinely-alive fbuild keeps its lock.pid_executable_pathreturningNone(permission denied, race) counts as alive, never dead.exe_stemexisted fall back to liveness alone.create_dirandwrite_lock_ownerare two steps, so a waiter can observe the gap. A lock with no owner record is left alone for a 30 s grace, then reclaimed — otherwise a crash between those two calls still wedged for the full 2 h.The issue also notes the diagnostic trap (a directory-as-mutex makes
File.Openreturn "Access is denied", reading as "held by a live process"). The reclaim path now logs the owning PID when it reclaims, so the state is legible.Part 2 — stale endpoint records
daemon stopnow clears the port/pid/status/owner-claim records when it finds no daemon. The early return was the bug: "daemon is not running" is exactly when those records are known to be garbage, and it was the one path that left them untouched.daemon_status.json. Sodaemon statuskept describing a dead PID after a clean exit, not just a crash.get_daemon_port()no longer trusts the port file verbatim — it's ignored when the recorded owner is provably gone, with the same fail-safe rules as above (a missing claim, including the startup window where the port file exists but the claim doesn't yet, counts as alive).What part 2 does not fix — stated plainly
This does not explain "clients keep dialing a dead port". The port is derived deterministically from (version, cache identity) per #1009, so discarding the port file yields the same number — which is precisely why deleting the files by hand didn't help you either. That symptom is a daemon-respawn failure, and pinning it down needs a repro I don't have.
So: this PR makes the recorded state stop lying about a dead daemon, which is what the issue asks for. If you want #1213 kept open for the respawn behaviour, say so and I'll re-open or split it rather than let it close silently.
Part 3 — undiagnosable esptool failure
cached esptool executable ... exited with status 2carried no output, which is what you hit on Windows with a freshly-installed 5.3.0. The error now includes captured stderr/stdout, and says so explicitly when both are empty rather than leaving the reader guessing which it was.Tests
9 new, all passing:
exe_stem; the create/write race window left alone; abandoned-mid-creation lock reclaimed after grace; owner record round-tripfbuild-daemonVerified locally:
-p fbuild-packages-fetch -p fbuild-library -p fbuild-pathsgreen, a full workspace check clean, clippy clean with-D warnings.🤖 Generated with Claude Code