428 lines
7.3 KiB
JavaScript
428 lines
7.3 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const db = require('../db/knex');
|
|
|
|
const {
|
|
processNextJob,
|
|
resetStaleProcessingJobs,
|
|
} = require('../api_v1/scanner/geocodeWorker');
|
|
|
|
const PROJECT_ROOT =
|
|
path.resolve(__dirname, '..');
|
|
|
|
const DEFAULT_LOCK_PATH =
|
|
path.join(PROJECT_ROOT, 'geocode-daemon.lock');
|
|
|
|
function parsePositiveInteger(
|
|
value,
|
|
optionName,
|
|
) {
|
|
const number = Number(value);
|
|
|
|
if (
|
|
!Number.isInteger(number)
|
|
|| number <= 0
|
|
) {
|
|
throw new Error(
|
|
`${optionName} deve essere un intero positivo`,
|
|
);
|
|
}
|
|
|
|
return number;
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const options = {
|
|
pollMs: 2000,
|
|
resetStaleMinutes: 30,
|
|
staleCheckMs: 60000,
|
|
lockPath:
|
|
process.env.GEOCODE_DAEMON_LOCK
|
|
|| DEFAULT_LOCK_PATH,
|
|
};
|
|
|
|
for (
|
|
let index = 0;
|
|
index < argv.length;
|
|
index += 1
|
|
) {
|
|
const argument = argv[index];
|
|
const value = argv[index + 1];
|
|
|
|
if (argument === '--poll-ms') {
|
|
if (value == null) {
|
|
throw new Error(
|
|
'Valore mancante dopo --poll-ms',
|
|
);
|
|
}
|
|
|
|
options.pollMs = parsePositiveInteger(
|
|
value,
|
|
'--poll-ms',
|
|
);
|
|
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
argument
|
|
=== '--reset-stale-minutes'
|
|
) {
|
|
if (value == null) {
|
|
throw new Error(
|
|
'Valore mancante dopo '
|
|
+ '--reset-stale-minutes',
|
|
);
|
|
}
|
|
|
|
options.resetStaleMinutes =
|
|
parsePositiveInteger(
|
|
value,
|
|
'--reset-stale-minutes',
|
|
);
|
|
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (argument === '--stale-check-ms') {
|
|
if (value == null) {
|
|
throw new Error(
|
|
'Valore mancante dopo '
|
|
+ '--stale-check-ms',
|
|
);
|
|
}
|
|
|
|
options.staleCheckMs =
|
|
parsePositiveInteger(
|
|
value,
|
|
'--stale-check-ms',
|
|
);
|
|
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (argument === '--lock-file') {
|
|
if (!value) {
|
|
throw new Error(
|
|
'Valore mancante dopo --lock-file',
|
|
);
|
|
}
|
|
|
|
options.lockPath = path.resolve(value);
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
throw new Error(
|
|
`Argomento sconosciuto: ${argument}`,
|
|
);
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function wait(milliseconds) {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, milliseconds);
|
|
});
|
|
}
|
|
|
|
function isProcessAlive(pid) {
|
|
if (
|
|
!Number.isInteger(pid)
|
|
|| pid <= 0
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch (error) {
|
|
return error.code === 'EPERM';
|
|
}
|
|
}
|
|
|
|
function readLock(lockPath) {
|
|
try {
|
|
const text = fs.readFileSync(
|
|
lockPath,
|
|
'utf8',
|
|
);
|
|
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function acquireLock(lockPath) {
|
|
const lockDirectory =
|
|
path.dirname(lockPath);
|
|
|
|
fs.mkdirSync(
|
|
lockDirectory,
|
|
{ recursive: true },
|
|
);
|
|
|
|
try {
|
|
const descriptor = fs.openSync(
|
|
lockPath,
|
|
'wx',
|
|
);
|
|
|
|
const lockData = {
|
|
pid: process.pid,
|
|
started_at:
|
|
new Date().toISOString(),
|
|
project_root: PROJECT_ROOT,
|
|
};
|
|
|
|
fs.writeFileSync(
|
|
descriptor,
|
|
JSON.stringify(lockData, null, 2)
|
|
+ '\n',
|
|
'utf8',
|
|
);
|
|
|
|
fs.closeSync(descriptor);
|
|
|
|
return;
|
|
} catch (error) {
|
|
if (error.code !== 'EEXIST') {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const existingLock = readLock(lockPath);
|
|
const existingPid =
|
|
Number(existingLock?.pid);
|
|
|
|
if (isProcessAlive(existingPid)) {
|
|
throw new Error(
|
|
`Daemon già attivo con PID `
|
|
+ `${existingPid}. Lock: ${lockPath}`,
|
|
);
|
|
}
|
|
|
|
console.warn(
|
|
'[geocodeDaemon] rimuovo lock obsoleto:',
|
|
{
|
|
lock_path: lockPath,
|
|
previous_pid:
|
|
existingLock?.pid ?? null,
|
|
},
|
|
);
|
|
|
|
fs.unlinkSync(lockPath);
|
|
|
|
acquireLock(lockPath);
|
|
}
|
|
|
|
function releaseLock(lockPath) {
|
|
const lock = readLock(lockPath);
|
|
|
|
if (
|
|
lock
|
|
&& Number(lock.pid) !== process.pid
|
|
) {
|
|
console.warn(
|
|
'[geocodeDaemon] lock appartenente '
|
|
+ 'a un altro processo, non rimosso:',
|
|
lockPath,
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
fs.unlinkSync(lockPath);
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') {
|
|
console.warn(
|
|
'[geocodeDaemon] impossibile '
|
|
+ 'rimuovere il lock:',
|
|
error.message,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getQueueStatus() {
|
|
const rows = await db('geocode_jobs')
|
|
.select('status')
|
|
.count({ count: '*' })
|
|
.groupBy('status');
|
|
|
|
const status = {
|
|
pending: 0,
|
|
processing: 0,
|
|
done: 0,
|
|
error: 0,
|
|
};
|
|
|
|
for (const row of rows) {
|
|
status[row.status] =
|
|
Number(row.count || 0);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArguments(
|
|
process.argv.slice(2),
|
|
);
|
|
|
|
let stopRequested = false;
|
|
let lastStaleCheck = 0;
|
|
|
|
acquireLock(options.lockPath);
|
|
|
|
const requestStop = (signal) => {
|
|
if (stopRequested) {
|
|
console.log(
|
|
`[geocodeDaemon] ${signal}: `
|
|
+ 'arresto già richiesto',
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
stopRequested = true;
|
|
|
|
console.log(
|
|
`[geocodeDaemon] ${signal}: `
|
|
+ 'arresto dopo il job corrente',
|
|
);
|
|
};
|
|
|
|
process.on(
|
|
'SIGINT',
|
|
() => requestStop('SIGINT'),
|
|
);
|
|
|
|
process.on(
|
|
'SIGTERM',
|
|
() => requestStop('SIGTERM'),
|
|
);
|
|
|
|
const initialReset =
|
|
await resetStaleProcessingJobs(
|
|
options.resetStaleMinutes,
|
|
);
|
|
|
|
lastStaleCheck = Date.now();
|
|
|
|
console.log(
|
|
'[geocodeDaemon] avviato',
|
|
{
|
|
pid: process.pid,
|
|
project_root: PROJECT_ROOT,
|
|
poll_ms: options.pollMs,
|
|
stale_check_ms:
|
|
options.staleCheckMs,
|
|
reset_stale_minutes:
|
|
options.resetStaleMinutes,
|
|
stale_jobs_reset: initialReset,
|
|
lock_path: options.lockPath,
|
|
queue: await getQueueStatus(),
|
|
},
|
|
);
|
|
|
|
let processed = 0;
|
|
let succeeded = 0;
|
|
let failed = 0;
|
|
let rescheduled = 0;
|
|
|
|
while (!stopRequested) {
|
|
const now = Date.now();
|
|
|
|
if (
|
|
now - lastStaleCheck
|
|
>= options.staleCheckMs
|
|
) {
|
|
const resetJobs =
|
|
await resetStaleProcessingJobs(
|
|
options.resetStaleMinutes,
|
|
);
|
|
|
|
lastStaleCheck = now;
|
|
|
|
if (resetJobs > 0) {
|
|
console.log(
|
|
'[geocodeDaemon] job obsoleti '
|
|
+ 'ripristinati:',
|
|
resetJobs,
|
|
);
|
|
}
|
|
}
|
|
|
|
const result = await processNextJob();
|
|
|
|
if (result) {
|
|
processed += 1;
|
|
|
|
if (result.status === 'done') {
|
|
succeeded += 1;
|
|
} else if (
|
|
result.status === 'error'
|
|
) {
|
|
failed += 1;
|
|
} else if (
|
|
result.status === 'pending'
|
|
) {
|
|
rescheduled += 1;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
await wait(options.pollMs);
|
|
}
|
|
|
|
console.log(
|
|
'[geocodeDaemon] terminato',
|
|
{
|
|
pid: process.pid,
|
|
jobs_processed: processed,
|
|
jobs_succeeded: succeeded,
|
|
jobs_failed: failed,
|
|
jobs_rescheduled: rescheduled,
|
|
queue: await getQueueStatus(),
|
|
},
|
|
);
|
|
}
|
|
|
|
let lockPath = null;
|
|
|
|
(async () => {
|
|
const options = parseArguments(
|
|
process.argv.slice(2),
|
|
);
|
|
|
|
lockPath = options.lockPath;
|
|
|
|
await main();
|
|
})()
|
|
.catch((error) => {
|
|
console.error(
|
|
'[geocodeDaemon] errore fatale:',
|
|
error.stack
|
|
|| error.message
|
|
|| error,
|
|
);
|
|
|
|
process.exitCode = 1;
|
|
})
|
|
.finally(async () => {
|
|
if (lockPath) {
|
|
releaseLock(lockPath);
|
|
}
|
|
|
|
await db.destroy();
|
|
});
|