407 lines
7 KiB
JavaScript
407 lines
7 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const db = require('../db/knex');
|
|
|
|
const {
|
|
ensureActivityIngestQueue,
|
|
resetStaleActivityIngestJobs,
|
|
getActivityIngestQueueCounts,
|
|
} = require(
|
|
'../api_v1/scanner/activityIngestQueue'
|
|
);
|
|
|
|
const {
|
|
runOneJob,
|
|
} = require(
|
|
'../api_v1/scanner/activityIngestWorker'
|
|
);
|
|
|
|
const PROJECT_ROOT = path.resolve(
|
|
__dirname,
|
|
'..'
|
|
);
|
|
|
|
const DEFAULT_LOCK_PATH = path.join(
|
|
PROJECT_ROOT,
|
|
'activity-ingest-daemon.lock'
|
|
);
|
|
|
|
function positiveInteger(value, option) {
|
|
const number = Number(value);
|
|
|
|
if (
|
|
!Number.isInteger(number) ||
|
|
number <= 0
|
|
) {
|
|
throw new Error(
|
|
`${option} deve essere un intero positivo`
|
|
);
|
|
}
|
|
|
|
return number;
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const options = {
|
|
pollMs: 2000,
|
|
resetStaleMinutes: 30,
|
|
staleCheckMs: 60000,
|
|
lockPath:
|
|
process.env.ACTIVITY_INGEST_DAEMON_LOCK ||
|
|
DEFAULT_LOCK_PATH,
|
|
help: false,
|
|
dryRun: false,
|
|
};
|
|
|
|
for (
|
|
let i = 0;
|
|
i < argv.length;
|
|
i += 1
|
|
) {
|
|
const arg = argv[i];
|
|
|
|
if (
|
|
arg === '--help' ||
|
|
arg === '-h'
|
|
) {
|
|
options.help = true;
|
|
continue;
|
|
}
|
|
|
|
if (arg === '--dry-run') {
|
|
options.dryRun = true;
|
|
continue;
|
|
}
|
|
|
|
const names = {
|
|
'--poll-ms': 'pollMs',
|
|
'--reset-stale-minutes':
|
|
'resetStaleMinutes',
|
|
'--stale-check-ms':
|
|
'staleCheckMs',
|
|
'--lock-file':
|
|
'lockPath',
|
|
};
|
|
|
|
const key = names[arg];
|
|
|
|
if (!key) {
|
|
throw new Error(
|
|
`Argomento sconosciuto: ${arg}`
|
|
);
|
|
}
|
|
|
|
const value = argv[i + 1];
|
|
|
|
if (
|
|
value == null ||
|
|
value.startsWith('--')
|
|
) {
|
|
throw new Error(
|
|
`Valore mancante dopo ${arg}`
|
|
);
|
|
}
|
|
|
|
options[key] =
|
|
key === 'lockPath'
|
|
? path.resolve(value)
|
|
: positiveInteger(value, arg);
|
|
|
|
i += 1;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
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 {
|
|
return JSON.parse(
|
|
fs.readFileSync(
|
|
lockPath,
|
|
'utf8'
|
|
)
|
|
);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function acquireLock(lockPath) {
|
|
fs.mkdirSync(
|
|
path.dirname(lockPath),
|
|
{ recursive: true }
|
|
);
|
|
|
|
try {
|
|
const fd = fs.openSync(
|
|
lockPath,
|
|
'wx'
|
|
);
|
|
|
|
try {
|
|
fs.writeFileSync(
|
|
fd,
|
|
JSON.stringify(
|
|
{
|
|
pid: process.pid,
|
|
started_at:
|
|
new Date().toISOString(),
|
|
project_root: PROJECT_ROOT,
|
|
},
|
|
null,
|
|
2
|
|
) + '\n',
|
|
'utf8'
|
|
);
|
|
} finally {
|
|
fs.closeSync(fd);
|
|
}
|
|
|
|
return;
|
|
} catch (error) {
|
|
if (error.code !== 'EEXIST') {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const existing =
|
|
readLock(lockPath);
|
|
|
|
const pid =
|
|
Number(existing?.pid);
|
|
|
|
if (isProcessAlive(pid)) {
|
|
throw new Error(
|
|
`Daemon gia attivo con PID ${pid}. ` +
|
|
`Lock: ${lockPath}`
|
|
);
|
|
}
|
|
|
|
if (
|
|
!existing ||
|
|
!Number.isInteger(pid) ||
|
|
pid <= 0
|
|
) {
|
|
throw new Error(
|
|
`Lock esistente non verificabile: ` +
|
|
`${lockPath}`
|
|
);
|
|
}
|
|
|
|
fs.unlinkSync(lockPath);
|
|
acquireLock(lockPath);
|
|
}
|
|
|
|
function releaseLock(lockPath) {
|
|
const lock =
|
|
readLock(lockPath);
|
|
|
|
if (
|
|
Number(lock?.pid) !== process.pid
|
|
) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
fs.unlinkSync(lockPath);
|
|
} catch (error) {
|
|
if (error.code !== 'ENOENT') {
|
|
console.error(
|
|
'[ActivityIngestDaemon] ' +
|
|
'Errore rimozione lock:',
|
|
error.message
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise(
|
|
resolve => setTimeout(resolve, ms)
|
|
);
|
|
}
|
|
|
|
async function main(options) {
|
|
if (options.help) {
|
|
console.log(
|
|
'Uso: node tools/runActivityIngestDaemon.js [opzioni]\n' +
|
|
' --dry-run\n' +
|
|
' --poll-ms N\n' +
|
|
' --reset-stale-minutes N\n' +
|
|
' --stale-check-ms N\n' +
|
|
' --lock-file PERCORSO'
|
|
);
|
|
return;
|
|
}
|
|
|
|
let stopping = false;
|
|
let lockAcquired = false;
|
|
let lastStaleCheck = 0;
|
|
|
|
const requestStop = signal => {
|
|
if (!stopping) {
|
|
console.log(
|
|
`[ActivityIngestDaemon] ` +
|
|
`Ricevuto ${signal}; arresto richiesto.`
|
|
);
|
|
|
|
stopping = true;
|
|
}
|
|
};
|
|
|
|
const onSigint =
|
|
() => requestStop('SIGINT');
|
|
|
|
const onSigterm =
|
|
() => requestStop('SIGTERM');
|
|
|
|
process.on('SIGINT', onSigint);
|
|
process.on('SIGTERM', onSigterm);
|
|
|
|
try {
|
|
acquireLock(options.lockPath);
|
|
lockAcquired = true;
|
|
|
|
await ensureActivityIngestQueue(db);
|
|
|
|
const recovered =
|
|
options.dryRun
|
|
? 0
|
|
: await resetStaleActivityIngestJobs(
|
|
db,
|
|
options.resetStaleMinutes
|
|
);
|
|
|
|
lastStaleCheck = Date.now();
|
|
|
|
console.log(
|
|
'[ActivityIngestDaemon] Avviato.',
|
|
{
|
|
pid: process.pid,
|
|
pollMs: options.pollMs,
|
|
staleCheckMs:
|
|
options.staleCheckMs,
|
|
dryRun: options.dryRun,
|
|
recovered,
|
|
queue:
|
|
await getActivityIngestQueueCounts(db),
|
|
}
|
|
);
|
|
|
|
while (!stopping) {
|
|
try {
|
|
if (options.dryRun) {
|
|
await sleep(options.pollMs);
|
|
continue;
|
|
}
|
|
|
|
const result =
|
|
await runOneJob(db);
|
|
|
|
if (result.error) {
|
|
console.error(
|
|
'[ActivityIngestDaemon] ' +
|
|
'Job fallito:',
|
|
result.error
|
|
);
|
|
}
|
|
|
|
/*
|
|
* Come Photo daemon:
|
|
* se ha processato un job, prova subito
|
|
* il successivo. Polling solo a coda vuota.
|
|
*/
|
|
if (
|
|
!result.processed &&
|
|
!stopping
|
|
) {
|
|
await sleep(options.pollMs);
|
|
}
|
|
|
|
if (
|
|
!stopping &&
|
|
Date.now() - lastStaleCheck >=
|
|
options.staleCheckMs
|
|
) {
|
|
const count =
|
|
await resetStaleActivityIngestJobs(
|
|
db,
|
|
options.resetStaleMinutes
|
|
);
|
|
|
|
lastStaleCheck = Date.now();
|
|
|
|
if (count) {
|
|
console.warn(
|
|
`[ActivityIngestDaemon] ` +
|
|
`Recuperati ${count} job bloccati.`
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
'[ActivityIngestDaemon] Errore ciclo:',
|
|
error
|
|
);
|
|
|
|
if (!stopping) {
|
|
await sleep(options.pollMs);
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
process.removeListener(
|
|
'SIGINT',
|
|
onSigint
|
|
);
|
|
|
|
process.removeListener(
|
|
'SIGTERM',
|
|
onSigterm
|
|
);
|
|
|
|
if (lockAcquired) {
|
|
releaseLock(options.lockPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
let options;
|
|
|
|
try {
|
|
options = parseArguments(
|
|
process.argv.slice(2)
|
|
);
|
|
|
|
await main(options);
|
|
} catch (error) {
|
|
console.error(
|
|
'[ActivityIngestDaemon] Errore fatale:',
|
|
error.message
|
|
);
|
|
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await db.destroy();
|
|
}
|
|
})();
|