Files
worldmonitor/scripts/seed-utils/runner.mjs
Jon Torrez bb79386d24 feat: seed orchestrator with auto-seeding, persistence, and management CLI (#1940)
* feat(docker): enable Redis RDB persistence, add seed-orchestrator to supervisord, copy scripts into image

* feat(orchestrator): add prefixed logger utility

* feat(orchestrator): add seed-meta read/write helpers

* feat(orchestrator): add child process runner with timeout support

* feat(orchestrator): add central seed catalog with 42 seeders

* feat(orchestrator): implement seed orchestrator with tiered scheduling

Adds scripts/seed-orchestrator.mjs with:
- classifySeeders: classify seeders into active/skipped by env vars
- buildStartupSummary: human-readable startup report
- Tiered cold start (hot/warm/cold/frozen with per-tier concurrency)
- Freshness check via seed-meta keys before running stale seeders
- Steady-state scheduling with setTimeout-based recurring timers
- Overlap protection, retry-after-60s, consecutive failure demotion
- Global concurrency cap of 5 with queue-based overflow
- Graceful shutdown on SIGTERM/SIGINT (15s drain timeout)
- Meta writing for null-metaKey seeders to seed-meta:orchestrator:{name}

* fix(seeds): use local API for warm-ping seeders in Docker mode

* fix(orchestrator): allow ACLED email+password as alternative to access token

* feat(wmsm): add seed manager CLI scaffold with help, catalog, and checks

* feat(wmsm): implement status command with freshness display

* feat(wmsm): implement schedule command with next-run estimates

* feat(wmsm): implement refresh command with single and --all modes

* feat(wmsm): implement flush and logs commands

* fix(wmsm): auto-detect docker vs podman runtime

* feat(orchestrator): extract pure scheduling functions and add test harness

* feat(orchestrator): add SEED_TURBO=real|dry mode with compressed intervals

* feat(orchestrator): add SEED_TURBO env passthrough and fix retry log message
2026-03-22 19:51:03 +04:00

65 lines
1.9 KiB
JavaScript

import { spawn } from 'node:child_process';
/**
* Fork a seed script as a child process and track its execution.
*
* @param {string} name — seeder name for logging
* @param {object} opts
* @param {string} opts.scriptPath — path to the script (or process.execPath for tests)
* @param {string[]} [opts.args] — arguments (default: none)
* @param {number} [opts.timeoutMs=120000] — kill after this many ms
* @param {object} [opts.env] — additional env vars (merged with process.env)
* @returns {Promise<{ name: string, exitCode: number|null, status: 'ok'|'error'|'timeout', durationMs: number }>}
*/
export function forkSeeder(name, opts) {
const { scriptPath, args = [], timeoutMs = 120_000, env } = opts;
return new Promise((resolve) => {
const start = Date.now();
const child = spawn(scriptPath, args, {
stdio: ['ignore', 'inherit', 'inherit'],
env: { ...process.env, ...env },
});
let settled = false;
const timer = setTimeout(() => {
if (!settled) {
settled = true;
child.kill('SIGTERM');
// Give 3s for graceful shutdown, then SIGKILL
setTimeout(() => {
try { child.kill('SIGKILL'); } catch {}
}, 3000);
resolve({ name, exitCode: null, status: 'timeout', durationMs: Date.now() - start });
}
}, timeoutMs);
child.on('close', (code) => {
if (!settled) {
settled = true;
clearTimeout(timer);
resolve({
name,
exitCode: code,
status: code === 0 ? 'ok' : 'error',
durationMs: Date.now() - start,
});
}
});
child.on('error', (err) => {
if (!settled) {
settled = true;
clearTimeout(timer);
resolve({
name,
exitCode: null,
status: 'error',
durationMs: Date.now() - start,
error: err.message,
});
}
});
});
}