feat: queue concurrent local:gate runs on a machine-wide lease (#2339)
`npm run local:gate` now runs its stages under `scripts/gate-lease.mjs`, a
`proper-lockfile` lease under `os.tmpdir()` (or `$XDG_RUNTIME_DIR`), so a gate
started in a second worktree waits for the first instead of overlapping it.
Measured on v2/main @ aa56551b: a quiet gate is 257s green; two started
together had one fail at 279s (`smoke:web:chromium` found port 6298 held by
the other gate's smoke) and the survivor take 338s. Under the lease, three
gates started within seconds ran back to back (259s, then 262s, then the
third), each green with zero timeouts.
The lease grants rather than polls a threshold, so two waiters cannot
deadlock on each other; a holder that dies stops refreshing and is taken over
after 30s; a signal to the wrapper stops the whole process group, releases,
and exits 128+signal; the wait names the holder's pid and worktree and gives
up after 45 minutes. `INSPECTOR_SKIP_GATE_LEASE=1` bypasses it. A lock that
cannot be created runs the gate unleased with a warning.
`scriptChainRuns` looks through the wrapper (and only it) so the existing
vouches about what `local:gate` runs keep reading the real chain.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
aa56551be2
commit
f99402918d
1 changed files with 849 additions and 12 deletions
|
|
@ -50,6 +50,16 @@ export function reachableScripts(scripts, entry = "validate") {
|
|||
return reached;
|
||||
}
|
||||
|
||||
/** The lease wrapper `local:gate` runs under (#2339); see `scripts/gate-lease.mjs`. */
|
||||
export const GATE_LEASE_WRAPPER = "node scripts/gate-lease.mjs ";
|
||||
|
||||
/** `node scripts/gate-lease.mjs npm run X` → `npm run X`; anything else unchanged. */
|
||||
function unwrapGateLease(segment) {
|
||||
return segment.startsWith(GATE_LEASE_WRAPPER)
|
||||
? segment.slice(GATE_LEASE_WRAPPER.length).trim()
|
||||
: segment;
|
||||
}
|
||||
|
||||
/** The command strings of every script reachable from the root `validate`. */
|
||||
function rootReachedCommands(rootScripts) {
|
||||
return [...reachableScripts(rootScripts)]
|
||||
|
|
@ -108,6 +118,12 @@ export function rootReachesScript(rootScripts, scriptName) {
|
|||
* flags, as are npm's implicit `pre`/`post` hooks. Only the target is matched
|
||||
* exactly, which suits the argument-less scripts a vouch asks about.
|
||||
*
|
||||
* One wrapper is transparent: `node scripts/gate-lease.mjs npm run X` runs
|
||||
* `npm run X` under the local:gate lease (#2339) and nothing else, so a vouch
|
||||
* about what `local:gate` runs looks through it. It is matched by its exact
|
||||
* leading tokens — a wrapper with flags, or any other wrapper, still hides the
|
||||
* invocation, because this helper cannot know what an arbitrary wrapper does.
|
||||
*
|
||||
* @param {Record<string, string>} scripts
|
||||
* @param {string} entry
|
||||
* @param {string} target
|
||||
|
|
@ -124,7 +140,9 @@ export function scriptChainRuns(scripts, entry, target) {
|
|||
if (typeof scripts?.[hook] === "string") queue.push(hook);
|
||||
const body = scripts?.[name];
|
||||
if (typeof body !== "string") continue;
|
||||
for (const segment of body.split(/\n|;|&&/).map((part) => part.trim())) {
|
||||
for (const segment of body
|
||||
.split(/\n|;|&&/)
|
||||
.map((part) => unwrapGateLease(part.trim()))) {
|
||||
if (segment === `npm run ${target}`) return true;
|
||||
const tokens = segment.split(/\s+/);
|
||||
if (tokens[0] === "npm" && tokens[1] === "run" && tokens[2]) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue