#!/usr/bin/env node /** * [목적] * heartbeat가 끊긴 자동매매 세션을 주기적으로 정리하는 백그라운드 워커입니다. * * [데이터 흐름] * worker loop -> /api/autotrade/worker/tick 호출 -> 만료 세션 sweep -> 로그 출력 */ const args = new Set(process.argv.slice(2)); const once = args.has("--once"); const appUrl = process.env.AUTOTRADE_APP_URL || "http://127.0.0.1:3001"; const workerToken = process.env.AUTOTRADE_WORKER_TOKEN || "autotrade-worker-local"; const pollMsRaw = Number.parseInt(process.env.AUTOTRADE_WORKER_POLL_MS || "5000", 10); const pollMs = Number.isFinite(pollMsRaw) ? Math.max(1000, pollMsRaw) : 5000; let isShuttingDown = false; function log(message, level = "INFO") { const time = new Date().toISOString(); const line = `[autotrade-worker][${level}][${time}] ${message}`; console.log(line); } async function runTick() { // Next API로 tick 요청을 보내 세션 만료 정리를 수행합니다. const response = await fetch(`${appUrl}/api/autotrade/worker/tick`, { method: "POST", headers: { "content-type": "application/json", "x-autotrade-worker-token": workerToken, }, body: JSON.stringify({}), }); const payload = await response.json().catch(() => null); if (!response.ok) { const message = payload?.message || `HTTP ${response.status}`; throw new Error(message); } return payload; } async function loop() { if (once) { const payload = await runTick(); log( `single tick done: expired=${payload?.sweep?.expiredCount ?? 0}, running=${payload?.runningSessions ?? 0}`, ); return; } log(`started (poll=${pollMs}ms, app=${appUrl})`); // 지정 주기마다 tick을 반복 호출합니다. while (!isShuttingDown) { try { const payload = await runTick(); log( `tick ok: expired=${payload?.sweep?.expiredCount ?? 0}, running=${payload?.runningSessions ?? 0}, stopped=${payload?.stoppedSessions ?? 0}`, ); } catch (error) { const message = error instanceof Error ? error.message : "tick failed"; log(message, "ERROR"); } await wait(pollMs); } log("graceful shutdown complete"); } function wait(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } process.on("SIGINT", () => { isShuttingDown = true; log("SIGINT received, stopping..."); }); process.on("SIGTERM", () => { isShuttingDown = true; log("SIGTERM received, stopping..."); }); loop().catch((error) => { const message = error instanceof Error ? error.message : "worker exited with error"; log(message, "ERROR"); process.exit(1); });