fix(update): reclaim a recovery lock whose owner PID is gone - #87166
Open
greqone wants to merge 2 commits into
Open
fix(update): reclaim a recovery lock whose owner PID is gone#87166greqone wants to merge 2 commits into
greqone wants to merge 2 commits into
Conversation
`_claim_recovery_lock` only ever broke a held `.update-incomplete.lock` once it was older than an hour, and never checked whether the process that wrote it still existed. A crashed or force-killed owner is exactly the state the marker system exists to recover from, so the lock it leaves behind is precisely the one that should be reclaimed soonest. Instead, every launch's early recovery became a silent no-op for up to an hour: the claim failed, `recover_if_needed` returned without printing anything, and a pending dependency install sat unfinished long after the process holding it had died. On Windows this compounds with the `"update" in argv` exclusion, since `hermes update` never reaches the recovery path itself, so the only code that could finish the install was the code being told to wait. Probe the PID recorded in the lock body via the module's existing `_pid_is_running` helper and reclaim immediately when it is gone. The age fallback stays for bodies that cannot be parsed (empty, truncated, older layouts) and for an owner that is alive but wedged, and a reclaimed lock is now retried on the same launch rather than costing an extra one. Exactly one retry, so a racer that wins the same reclaim is yielded to rather than spun on. `recover_if_needed` carried an inline copy of the claim with the same bug; it now calls the shared helper, so both paths are fixed by construction. Tests: dead owner reclaimed (both mocked and against a real reaped PID), live owner still respected, age fallback still breaks a wedged live owner, unparseable bodies fall through to the age fallback, a racer recreating the lock returns False instead of looping, and an end-to-end case proving a stale lock no longer blocks the early repair. Nine of the ten fail without this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
|
@gemini-code-assist @devin-ai-integration @chatgpt-codex-connector please review. Focus areas, since this touches a lock:
|
Contributor
fix(update): reclaim a recovery lock whose owner PID is gone The liveness-probe + age-fallback design is sound, and the conservative direction (unreadable/non-numeric body → treat as live → wait for age fallback) is the right call for a lock that must never be stolen from a real owner. Observations:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
_claim_recovery_lockdecides whether a held.update-incomplete.lockmay be broken purely by age: older than one hour, take it; otherwise give up. It never asks whether the process that wrote the lock still exists.That gets the priority exactly backwards. A crashed or force-killed owner is the precise state the marker system exists to recover from, so its abandoned lock is the one that should be reclaimed soonest. Instead it is honoured for a full hour, and honoured silently: the claim fails,
recover_if_neededreturns without printing anything, and a pending dependency install sits unfinished long after the process holding it died. From the outside the CLI looks healthy and simply never repairs itself.On Windows this compounds with the
"update" in argvexclusion a few lines up.hermes updatedeliberately never runs the recovery path, so the only code that can finish a deferred install is the early pass, which is the code being told to wait. I hit this on a real install: the lock was owned by PID 59912, dead for twenty minutes, and three consecutivehermes doctorruns each no-opped without a word while the pending install stayed pending.The fix reuses this module's own
_pid_is_runninghelper, which already exists a few hundred lines above and already handles the Win32OpenProcesscase. Read the PID out of the lock body, and if that process is gone, reclaim the lock immediately.Deliberately preserved:
_lock_owner_is_liveis conservative: anything unreadable or non-numeric counts as live, so an unidentifiable lock is never stolen on liveness grounds.Two behaviour changes beyond the probe, both small:
False, so even a successful break cost an extra launch.recover_if_neededcarried an inline copy of the claim logic with the identical bug. It now calls the shared helper, so both paths are fixed by construction rather than by remembering to patch two places.Related Issue
Related to #86943 — that issue's headline symptom is the pre-#86735 preflight, fixed by #86781. This is the other reason a deferral fails to resume, and it survives that fix: once the preflight correctly defers, an orphaned lock can still keep the marker recovery from running for an hour.
Not a duplicate of #86782, #86826 or #86857, none of which touch the lock.
Type of Change
Changes Made
hermes_cli/_early_recovery.py_lock_owner_is_live()— parses the bare PID from the lock body and defers to the existing_pid_is_running(); conservative on anything it cannot parse_discard_stale_recovery_lock()— removes a lock whose owner is gone, or which has aged out_RECOVERY_LOCK_MAX_AGE_SECONDSreplaces the bare3600literal_claim_recovery_lock()retries once after a successful discardrecover_if_needed()drops its inline duplicate of the claim and calls the helpertests/hermes_cli/test_early_recovery.py— ten new tests (below)How to Test
.lazy-refresh-incompletemarker and write.update-incomplete.lockcontaining the PID of a process that has already exited._claim_recovery_lock(root)returnsFalse, andrecover_if_neededperforms no repair, for one hour, printing nothing.Automated:
Nine of the ten new tests fail on
mainwithout the source change; the tenth (test_recovery_lock_yields_to_a_live_owner) passes both before and after by design, as it guards the single-flight behaviour this must not break.New coverage:
..._reclaimed_when_owner_pid_is_dead..._reclaimed_from_a_real_dead_pid_pid_is_runningis exercised for real on each OS..._yields_to_a_live_owner..._age_fallback_breaks_a_wedged_live_owner..._unparseable_body_waits_for_the_age_fallback..._does_not_loop_when_a_racer_recreates_itFalseinstead of spinningtest_stale_lock_no_longer_blocks_the_early_repairAdjacent suites run clean:
test_early_recovery.py,test_update_self_lock.py,test_update_interrupted_recovery.py,test_checkout_mutation_guards.py— 56 passed.ruff check .passes.ruff formatis not applied:maindoes not satisfy it on these files either, and CI enforces onlyruff check, so reformatting would have added unrelated churn.Checklist
Code
Documentation & Housekeeping
docs/, docstrings) — docstrings only, no user-facing doc changecli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A_pid_is_running, which already branches Win32OpenProcessvs POSIXos.kill(pid, 0); the real-dead-PID test covers both. Module stays stdlib-only, which is load-bearing here.🤖 Generated with Claude Code