GitNexus/gitnexus/test/fixtures/index-lock-child.mjs
azizur100389 3f5ca8cdb7
fix(storage): guard stale file-lock reclamation (#3234)
* fix(storage): guard stale file-lock reclamation

* fix(storage): close lock recovery failure paths

* Address PR review feedback (#3234)

- Flush the lock-child stderr diagnostic before process.exit
- Treat explicit NaN timeouts as the default ceiling
- Document non-retryable guard timeouts on the worker IPC contract

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): stop mislabeling live lock waits as orphan recovery

A brief peer inspect must not attach guardPath or send operators to
RUNBOOK delete steps. Refuse lock-free embeddings sync, stop --watch
only on a true guard timeout, and drop an unreadable self-created
guard before failing closed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* Address PR review feedback (#3234)

- Verify lock/guard absence before degrading a denied main-lock create
- Launch the third contender from unlinkSync, not the dead rename path
- Document group-lock timeouts for unrecoverable guard leftovers

Co-authored-by: Cursor <cursoragent@cursor.com>

* style: prettier index-lock reclaim guard tests

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): treat O_EXCL as the lock-file presence check

CodeQL flagged existsSync-then-wx on analyze.lock. Create with wx first and
only reclaim unreadable leftovers after grace, so a successor is never
unlinked from a lost race.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-12 11:47:12 +01:00

67 lines
2.8 KiB
JavaScript

/**
* Child process for the cross-process index-lock tests (#2658), using the BUILT
* module (LOCK_MODULE). Two modes:
*
* - default (HOLD): acquire the lock on LOCK_DIR, write MARKER once held, then
* hold until killed. Proves real cross-process exclusion and SIGKILL
* kill-recovery against a parent that uses the source module.
*
* - MODE=EXCLUSIVE (SENTINEL set): acquire, then enter a critical section
* guarded by an O_EXCL sentinel create — if the sentinel already exists,
* another process holds the lock at the same time, which is the exact
* single-writer violation the test hunts. Hold briefly, remove the sentinel,
* release, exit 0. Exit 3 only if the sentinel was already present (overlap),
* exit 4 for other sentinel-create failures, with stderr diagnostics.
* Used by the multi-reclaimer test where ≥2 children reclaim one dead holder.
*/
import { writeFileSync, writeSync, openSync, closeSync, unlinkSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
// LOCK_MODULE is an absolute path. On Windows `import('C:\\…')` throws
// ERR_UNSUPPORTED_ESM_URL_SCHEME (a bare drive path is read as a URL scheme), so
// convert to a file:// URL — required on Windows, harmless on POSIX.
const { acquireIndexLock } = await import(pathToFileURL(process.env.LOCK_MODULE).href);
if (process.env.MODE === 'EXCLUSIVE') {
const lock = await acquireIndexLock(process.env.LOCK_DIR, { timeoutMs: 30_000, pollMs: 25 });
try {
// O_EXCL create fails if any other process is simultaneously in its own
// critical section — that is a broken single-writer invariant.
let fd;
try {
fd = openSync(process.env.SENTINEL, 'wx');
} catch (error) {
// writeSync(2) flushes before process.exit; console.error on a piped
// stderr can be truncated when the child is spawned with stdio: pipe.
writeSync(
2,
`Sentinel create failed: pid=${process.pid} path=${process.env.SENTINEL} ` +
`code=${error.code ?? 'unknown'} message=${error.message}\n`,
);
lock.release();
process.exit(error.code === 'EEXIST' ? 3 : 4);
}
closeSync(fd);
// Hold the section briefly so concurrent reclaimers would collide here.
await new Promise((r) => setTimeout(r, 150));
unlinkSync(process.env.SENTINEL);
} finally {
lock.release();
}
process.exit(0);
} else {
const lock = await acquireIndexLock(process.env.LOCK_DIR, { timeoutMs: 30_000, pollMs: 25 });
writeFileSync(process.env.MARKER, String(process.pid));
// Hold the lock until the parent kills us.
setInterval(() => {}, 1000);
// Release on a graceful signal (the SIGKILL path in the test never reaches this).
const release = () => {
try {
lock.release();
} finally {
process.exit(0);
}
};
process.on('SIGTERM', release);
process.on('SIGINT', release);
}