Phase 1: lock cron, reload chaud, argon2, providers, IMDb lookup, cache LRU, /health, /metrics, rate limit, UI dark, biome
This commit is contained in:
60
lib/lockFile.js
Normal file
60
lib/lockFile.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// Simple PID-based lock file. Atomic via O_EXCL.
|
||||
// If a stale lock is found (PID no longer alive), it is removed.
|
||||
|
||||
import { closeSync, openSync, readFileSync, unlinkSync, writeSync } from 'node:fs';
|
||||
|
||||
function isAlive(pid) {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return e.code === 'EPERM';
|
||||
}
|
||||
}
|
||||
|
||||
export function acquireLock(lockPath) {
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
try {
|
||||
const fd = openSync(lockPath, 'wx');
|
||||
writeSync(fd, String(process.pid));
|
||||
closeSync(fd);
|
||||
const release = () => {
|
||||
try {
|
||||
unlinkSync(lockPath);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
process.on('exit', release);
|
||||
process.on('SIGINT', () => {
|
||||
release();
|
||||
process.exit(130);
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
release();
|
||||
process.exit(143);
|
||||
});
|
||||
return release;
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
// Lock exists — check if owner is still alive
|
||||
let pid = 0;
|
||||
try {
|
||||
pid = parseInt(readFileSync(lockPath, 'utf8').trim(), 10);
|
||||
} catch {
|
||||
/* unreadable */
|
||||
}
|
||||
if (pid && isAlive(pid)) {
|
||||
throw new Error(`Cron already running (PID ${pid}, lock ${lockPath})`);
|
||||
}
|
||||
// Stale lock — remove and retry
|
||||
console.log(`Removing stale lock (PID ${pid || '?'} no longer alive): ${lockPath}`);
|
||||
try {
|
||||
unlinkSync(lockPath);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not acquire lock after retries: ${lockPath}`);
|
||||
}
|
||||
Reference in New Issue
Block a user