47 lines
1.5 KiB
JavaScript
47 lines
1.5 KiB
JavaScript
|
|
// Generic exclusive file lock, same idiom as calendar.mjs but reusable for any
|
||
|
|
// critical section. Used for the global BUILD lock so cron builds (promoteDraft)
|
||
|
|
// and Developer Console builds (preview + publish) never run npm/astro
|
||
|
|
// concurrently — important on this memory-constrained box.
|
||
|
|
import { writeFileSync, openSync, closeSync, unlinkSync, statSync } from "node:fs";
|
||
|
|
|
||
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Acquire an exclusive lockfile, run fn, always release.
|
||
|
|
* @param {string} lockPath absolute path to the lockfile
|
||
|
|
* @param {() => Promise<T>|T} fn critical section
|
||
|
|
* @param {{timeoutMs?: number, staleMs?: number}} [opts]
|
||
|
|
* @returns {Promise<T>}
|
||
|
|
*/
|
||
|
|
export async function withLock(lockPath, fn, { timeoutMs = 300000, staleMs = 15 * 60 * 1000 } = {}) {
|
||
|
|
const start = Date.now();
|
||
|
|
for (;;) {
|
||
|
|
try {
|
||
|
|
const fd = openSync(lockPath, "wx"); // exclusive create — fails if it exists
|
||
|
|
writeFileSync(lockPath, `${process.pid} ${new Date().toISOString()}`);
|
||
|
|
closeSync(fd);
|
||
|
|
break;
|
||
|
|
} catch {
|
||
|
|
try {
|
||
|
|
if (Date.now() - statSync(lockPath).mtimeMs > staleMs) {
|
||
|
|
unlinkSync(lockPath); // presumed orphaned
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
/* lock vanished between failed create and stat — retry immediately */
|
||
|
|
}
|
||
|
|
if (Date.now() - start > timeoutMs) throw new Error(`lock timeout: ${lockPath}`);
|
||
|
|
await sleep(200);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
return await fn();
|
||
|
|
} finally {
|
||
|
|
try {
|
||
|
|
unlinkSync(lockPath);
|
||
|
|
} catch {
|
||
|
|
/* already gone */
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|