Skip to content

fix(packages,daemon): reclaim dead-owner install locks; stop leaving stale endpoint records (#1213) - #1227

Merged
zackees merged 2 commits into
mainfrom
fix/1213-stale-install-locks
Aug 1, 2026
Merged

fix(packages,daemon): reclaim dead-owner install locks; stop leaving stale endpoint records (#1213)#1227
zackees merged 2 commits into
mainfrom
fix/1213-stale-install-locks

Conversation

@zackees

@zackees zackees commented Aug 1, 2026

Copy link
Copy Markdown
Member

Addresses #1213 parts 1, 2 and 3.

Part 1 — the deadlock (the one that cost 40 minutes)

The headline finding: owner.txt was already recording the holder's pid=. 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 — matching the reported ~40 min of waiting for another process to install ... with no fbuild process alive. The data needed to fix this was already on disk.

lock_is_stale now reclaims a lock when its owner is gone. Three ordered reasons, deliberately ordered by confidence:

  1. the directory vanished;
  2. the owner process is dead (new);
  3. the age ceiling — kept as the backstop for what liveness can't answer (a hung-but-running peer, a missing owner record).

Because the failure mode of getting this wrong is deleting a live install's lock, every probe fails safe toward "alive":

  • PID recycling is covered by also recording 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_path returning None (permission denied, race) counts as alive, never 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, 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.Open return "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 stop now 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.
  • Found an omission the issue didn't mention: 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.
  • 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 2 carried 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:

  • 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; abandoned-mid-creation lock reclaimed after grace; owner record round-trip
  • the endpoint liveness gate fails safe with no claim; the test binary is not mistaken for fbuild-daemon

Verified locally: -p fbuild-packages-fetch -p fbuild-library -p fbuild-paths green, a full workspace check clean, clippy clean with -D warnings.

🤖 Generated with Claude Code

zackees and others added 2 commits August 1, 2026 14:43
… 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>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@zackees, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: be674fc2-1e08-4dd3-93dc-13b08c680e8b

📥 Commits

Reviewing files that changed from the base of the PR and between dd1ddd4 and 93ad12a.

📒 Files selected for processing (5)
  • crates/fbuild-cli/src/cli/daemon_cmd.rs
  • crates/fbuild-daemon/src/main.rs
  • crates/fbuild-library/src/library/esptool.rs
  • crates/fbuild-packages-fetch/src/install_lock.rs
  • crates/fbuild-paths/src/lib.rs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zackees
zackees merged commit 253d6a7 into main Aug 1, 2026
90 of 93 checks passed
zackees added a commit that referenced this pull request Aug 1, 2026
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>
@zackees
zackees deleted the fix/1213-stale-install-locks branch August 1, 2026 23:12
zackees added a commit that referenced this pull request Aug 2, 2026
…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>
@fastled-project-sync fastled-project-sync Bot moved this to Triage in FastLED Tracker Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

1 participant