Spaces:
Sleeping
Sleeping
File size: 4,742 Bytes
592c446 247f49f 8df2478 592c446 8df2478 592c446 8df2478 592c446 8df2478 592c446 8df2478 592c446 247f49f 592c446 8df2478 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 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);
});
|