GitNexus/gitnexus/test/fixtures/index-lock-child.mjs

70 lines
3 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, renameSync } 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 });
// The parent treats existence as readiness, so publish only the complete PID.
const pendingMarker = `${process.env.MARKER}.${process.pid}.tmp`;
writeFileSync(pendingMarker, String(process.pid));
renameSync(pendingMarker, process.env.MARKER);
// 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);
}