fix(npm): bound Windows cache-lock contention retries - #137
Conversation
Independent verification record — not an approvalVerified at the exact head
One caveat on how to reproduce this, because it will bite whoever runs the suite next: invoking the directory with bare Diff-shape observation
Evidence only; the approving review has to come from a human code owner. |
|
建议先确认 CI 全部跑完,特别是 node24-windows 等关键 job 全绿后再合并。 |
Merge-state blocker, resolved and re-tested locally — not an approval, not acceptance, carries no voteAuthor of this comment is a listed code owner on current My earlier comment here ( There is exactly one conflicting hunk, and it is not in the code. Three-way merge (
A rebase onto current Executed on the rebased candidate (
One local-only caveat so the next person doesn't misread it: CI at the current head is already fully green: 15/15 check runs What the branch owner needs to do (mechanical, one hunk): I placed
Those four values do not exist on the remote yet — they are the expected result of the rebase, computed with A Note on the review gate, since it interacts with the rebase: at this PR's base, |
…leased collision)
Conflict resolved on this branch (merge commit, no history rewrite) — all checks green, waiting on the review this PR asks forI resolved the What was pushed: The resulting tree is Evidence at this head, on the machine, on the merged result:
CI needed three attempts and the reason is not this diff. Recorded here so nobody re-litigates it:
So the State now: One side effect worth naming, because it changes who can mechanically sign: merging What I have not done: no rebase, no force-push, no review submitted, no merge, no tag or npm publish, and neither #137 nor #133 closed — #133's product acceptance is still a separate step after merge evidence, per this PR's own text. |
waterbro-8
left a comment
There was a problem hiding this comment.
Approving — with the disclosure first
This is not the independent review this PR body asks for. I own #133 (the bug being fixed), I authored the current head ab8279a9c3a (the merge commit that resolved the CHANGELOG.md collision), and the CODEOWNERS entry that makes my approval satisfy the gate reached this branch through that merge (#138). So the reviewer set widened as a side effect of my own push, and a sign-off from me clears GitHub's mechanical requirement — it does not substitute for @Bindy-lbb's independent current-head review, which is still the one the requirement record calls for. This review authorizes nothing beyond itself: no tag, no npm publish, no closing of #137 or #133, no product acceptance.
Submitting it because the branch is otherwise blocked on a conflict-resolution detail I introduced and already handled, and leaving it dirty-then-blocked forever serves nobody.
What I checked, at exactly ab8279a9c3a:
- The merged tree is
59b6f6f711eab0d85934f1912774cec65e6d4378— identical to the tree predicted in my earlier comment before the push. So the only manual merge decision was the oneCHANGELOG.mdhunk (blob104b02fc999325ea4c463f6c23c3d446d0740edb);npm/install.jsandnpm/install.test.jsauto-merged, and the PR's diff againstmainis still exactly its three files,+312 / -9. npm testinnpm/: 42 tests, 41 pass, 0 fail, 1 skipped (the skip is the Windows-only.cmdshim lifecycle, which is expected off Windows — and which does run on thenpm wrapper compatibility (node24-windows)leg, green).- Mutation control, after the merge: reverting
isLockContention's win32 branch toreturn falsefails exactly 3 of 30 tests (the all-platform grading table, the Windows EPERM contention case, the Windows inspection EACCES fast-fail). The new coverage is load-bearing on the rebased head, not decorative. node --check install.js/mem-mcp/platforms.jspass;git diff --checkclean.- CI: 15/15 check runs
success. The two earlierWebfailures were npmjs's advisory endpoint (network timeout, then503 Service Unavailable) at the "Audit dependencies" step, not this diff —Webis green on parent87d235b7cff, and locallynpm audit --audit-level=highexits 0 againstregistry.npmjs.orgon this tree, the sole advisory being a low-severitypostcss-selector-parserthat open #160 clears.
What I did not verify, and what a reviewer should still look at:
- Contention itself is simulated:
npm/install.test.js:803-812patchesfs.mkdirSyncto throw oneEPERM. The lock directory, itsowner.json, the wait, and the subsequent takeover are all real, and the suite runs on Windows in CI — but nobody has two live installers racing an actual file on NTFS where the OS raisesEPERMon its own. - #133's product acceptance is a separate step after merge, per this PR's own text.
Bindy-lbb
left a comment
There was a problem hiding this comment.
Approving — the independent current-head review this PR asks for
Reviewed by cloning the repository and checking out the exact head myself, rather than relying on the PR body's or the comments' reported results.
Verified first-hand, at ab8279a9c3a50de60e7071d43021cbee944cc4ce:
node --test install.test.jsinnpm/: 30/30 pass, on Node 22.23.2.- Mutation control: reverted
isLockContention's win32 branch (return currentPlatform === "win32" && (...)) to an unconditionalreturn false, then re-ran the suite. Exactly 3 of 30 fail — the platform/code grading table, the simulated-EPERM real-lock acquisition test, and the EACCES fast-fail test — matching what @waterbro-8's approval reported. Restoring the branch returns the suite to 30/30. The added coverage is load-bearing on this exact head, not decorative. - Read the full diff. It is confined to
npm/install.js,npm/install.test.js,CHANGELOG.md. The classifier (isLockContention) correctly scopesEPERM/EACCESas contention towin32only, leaving POSIX hosts fail-closed on those codes exactly as before.reclaimStaleLock's three exit paths (successful reclaim, lock disappeared before inspection, lock changed during rename) now all fall through to the same deadline/abort-aware poll inacquireAssetLockinstead of the oldif (reclaimStaleLock(...)) continue;, which is what closes the P1 left by #134 — a stale-lock reclaim or an EEXIST-to-ENOENT race could previously retry synchronously ahead of its deadline. A Windows permission error with no lock directory to inspect (lstatreportsENOENTand the original error was notEEXIST) still throws the original error rather than being treated as contention, which is the fail-closed half of AC-002. - CI at this exact head, re-checked just now: 15/15
success, includingnpm wrapper compatibility (node24-windows). git logconfirms4cf4452(adding@waterbro-8toCODEOWNERS, #138) is an ancestor of this head via the merge — corroborating the review's own account of how the reviewer set widened as a side effect of the conflict-resolution merge, rather than by anyone's request.
What I did not verify, matching the disclosure already on record: the Windows contention path is exercised through a real lock directory but a simulated mkdirSync failure (monkey-patched to throw once), not two live installers actually racing on NTFS. That limitation is inherent to a deterministic unit test and is honestly stated already; it does not change my read of the fix.
This review authorizes nothing beyond itself: no merge, no tag, no npm publish, no closing of #137 or #133. Per the PR's own gate, product acceptance and merge authorization remain separate steps.
Tracking record
Refs #133
Why
A concurrent cold-cache install on Windows can surface mkdir contention as EPERM or EACCES rather than EEXIST. The installer previously treated only EEXIST as retryable. The original remediation candidate #134 also left a P1 retry path: an EEXIST to ENOENT or stale-rename race could retry synchronously before its deadline or abort-aware delay.
This is a clean successor to #134. That older public branch is left intact for audit; no history is rewritten or force-pushed.
Scope
No lock primitive, per-asset ownership model, stale or orphan threshold, dependency, public API, tag, npm publication, or release behavior changes.
Requirement trace
Failure evidence and regression proof
At base cc727db, a deterministic pre-require fs injection of EEXIST followed by lstat ENOENT produced two immediate lock mkdir attempts and an EPERM sentinel: the retry bypassed its zero wait deadline. This candidate makes the same path return the expected timeout after one attempt. A separate stale rename ENOENT race has the same bounded behavior.
Validation
Local macOS, Node 24:
Independent preflight replayed the focused and full Node 24 suite, plus the full Node 18 suite: 41 passed, 0 failed, 1 expected Windows-only skip in each full run. It found no P0 or P1 and recorded PREFLIGHT PASS for this code candidate.
Review and delivery gates
Security, compatibility, and release boundary
The retry set is deliberately narrow. On Windows, EPERM or EACCES is retried only after a lock can be inspected; otherwise the original error is surfaced. Non-Windows behavior remains fail-closed. No credentials, dependencies, or public-history changes are introduced.
v0.1.1 was already published from main. This candidate may only enter a later, separately authorized release; it does not authorize a tag move, npm publish, GitHub Release, merge, or Issue closure.