fde-worldcup-data-worker / scripts /worldcup /worldcup-sync-daemon.mjs
wihaha's picture
fix: trigger overdue sync on health checks
8df2478
Raw
History Blame Contribute Delete
4.74 kB
import { execFile } from 'node:child_process';
import http from 'node:http';
import { promisify } from 'node:util';
import { loadLocalEnv } from '../gaokao/lib/env.mjs';
loadLocalEnv();
const execFileAsync = promisify(execFile);
const intervalMinutes = Number(process.env.WORLDCUP_SYNC_INTERVAL_MINUTES || 30);
const stopAt = new Date(process.env.WORLDCUP_SYNC_UNTIL || '2026-07-20T00:00:00Z');
const intervalMs = Math.max(5, intervalMinutes) * 60 * 1000;
const port = Number(process.env.PORT || 7860);
const runTimeoutMs = Number(process.env.WORLDCUP_SYNC_RUN_TIMEOUT_MS || 10 * 60 * 1000);
let timer = null;
let runPromise = null;
const health = {
status: 'starting',
started_at: new Date().toISOString(),
last_run_started_at: null,
last_run_finished_at: null,
last_run_status: null,
last_error: null,
next_run_at: null,
stop_at: stopAt.toISOString(),
};
function isStopped() {
return new Date() >= stopAt;
}
function isDue() {
if (health.status !== 'waiting' || !health.next_run_at) return false;
return new Date(health.next_run_at).getTime() <= Date.now();
}
function scheduleNextRun(from = new Date()) {
if (timer) clearTimeout(timer);
timer = null;
if (isStopped()) {
health.status = 'stopped';
health.next_run_at = null;
console.log('[worldcup-sync-daemon] stop date reached; no more runs scheduled.');
return;
}
const nextAtMs = Math.min(from.getTime() + intervalMs, stopAt.getTime());
const waitMs = Math.max(0, nextAtMs - Date.now());
health.next_run_at = new Date(nextAtMs).toISOString();
console.log(`[worldcup-sync-daemon] next run in ${Math.round(waitMs / 60000)} minutes`);
timer = setTimeout(() => {
triggerRun('timer').catch((error) => {
console.error('[worldcup-sync-daemon] timer trigger failed:', error.message || error);
});
}, waitMs);
}
async function triggerRun(reason) {
if (runPromise) return runPromise;
if (isStopped()) {
health.status = 'stopped';
health.next_run_at = null;
return null;
}
console.log(`[worldcup-sync-daemon] run triggered by ${reason}`);
runPromise = (async () => {
await runOnce();
scheduleNextRun(new Date());
})().finally(() => {
runPromise = null;
});
return runPromise;
}
function startHealthServer() {
const server = http.createServer((req, res) => {
if (req.url === '/' || req.url === '/healthz') {
const dueTriggered = isDue();
if (dueTriggered) {
triggerRun('health-request-overdue').catch((error) => {
console.error('[worldcup-sync-daemon] overdue health trigger failed:', error.message || error);
});
}
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ ...health, due_triggered: dueTriggered }, null, 2));
return;
}
res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ error: 'not_found' }));
});
server.listen(port, '0.0.0.0', () => {
console.log(`[worldcup-sync-daemon] health server listening on 0.0.0.0:${port}`);
});
}
async function runOnce() {
const startedAt = new Date();
health.status = 'running';
health.last_run_started_at = startedAt.toISOString();
health.last_run_finished_at = null;
health.last_run_status = 'running';
health.last_error = null;
health.next_run_at = null;
console.log(`[worldcup-sync-daemon] sync started at ${startedAt.toISOString()}`);
try {
const result = await execFileAsync(process.execPath, ['scripts/worldcup/sync-worldcup-data.mjs'], {
cwd: process.cwd(),
maxBuffer: 1024 * 1024 * 20,
timeout: runTimeoutMs,
killSignal: 'SIGTERM',
});
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
health.status = 'waiting';
health.last_run_status = 'success';
health.last_run_finished_at = new Date().toISOString();
console.log(`[worldcup-sync-daemon] sync finished in ${Math.round((Date.now() - startedAt.getTime()) / 1000)}s`);
} catch (error) {
if (error.stdout) process.stdout.write(error.stdout);
if (error.stderr) process.stderr.write(error.stderr);
health.status = 'waiting';
health.last_run_status = 'failed';
health.last_run_finished_at = new Date().toISOString();
health.last_error = error.message || String(error);
console.error('[worldcup-sync-daemon] sync failed:', error.message || error);
}
}
console.log(`[worldcup-sync-daemon] interval=${intervalMinutes} minutes stop_at=${stopAt.toISOString()}`);
startHealthServer();
triggerRun('startup').catch((error) => {
console.error('[worldcup-sync-daemon] startup trigger failed:', error.message || error);
});