/* tools/lib/pool.mjs — 한 대의 기계에서 N개 자식 프로세스로 작업을 흘려보내는 풀. 이 저장소의 계기판은 전부 (슬롯 × 시드 × 쌍) 같은 **서로 독립인 작업**을 순차로 돌린다. ⑤ 증명기가 그 한계에 먼저 부딪혔다 — 쌍 하나에 최대 150초, 슬롯 32개면 하루가 간다. 여기서 병렬화의 단위는 **작업**이지 파일이 아니다. 작업마다 프로세스를 새로 띄우면 engine.js(1.5MB) 를 매번 다시 읽으므로, 워커는 **영속**이고 작업만 흘려보낸다. 프로토콜은 stdout 의 JSON 한 줄이다: 부모 -> 워커 {"id": 7, "job": {...}} 워커 -> 부모 {"id": 7, "ok": true, "result": {...}} 또는 {"id":7,"ok":false,"err":"..."} 워커는 **stdout 에 그 외 아무것도 쓰면 안 된다**. 진행 로그는 stderr 로 보낸다 (부모가 그대로 흘려보내므로 사람은 그대로 읽는다). 시간 한계를 가진 작업의 주의 (measured, 2026-08-01): 병렬로 돌리면 같은 작업이 더 오래 걸린다. 그래서 이 저장소의 판정은 **상태 한계**(결정적)를 1순위로 두고 시간 한계는 안전망으로만 쓴다. 어느 한계에 걸렸는지 결과에 적는 것은 워커의 책임이다 — 그래야 "8개로 돌렸더니 미판정이 늘었다"가 발견이 아니라 부하라는 것을 구별할 수 있다. */ import { spawn } from 'node:child_process'; import os from 'node:os'; export function poolSize(req) { const n = Number(req) || 0; if (n > 0) return n; return Math.max(2, Math.min(8, os.cpus().length - 2)); } /* runPool(jobs, workerPath, opts) -> Promise> 결과는 **입력 순서**로 돌아온다(완료 순서가 아니다 — 보고가 실행 순서에 흔들리면 같은 명령이 다른 표를 낸다). opts.onDone(i, result) 은 완료 즉시 불린다(진행 표시용). 워커가 죽으면 그 작업은 null 이고, 남은 작업은 다른 워커가 계속 가져간다. */ export async function runPool(jobs, workerPath, opts = {}) { const N = Math.min(poolSize(opts.concurrency), Math.max(1, jobs.length)); const results = new Array(jobs.length).fill(null); let cursor = 0; let done = 0; const kids = []; const startKid = () => { const kid = spawn(process.execPath, [workerPath, ...(opts.argv || [])], { cwd: opts.cwd || process.cwd(), env: { ...process.env, ...(opts.env || {}) }, stdio: ['pipe', 'pipe', 'inherit'], }); const st = { kid, busy: -1, buf: '' }; kid.stdout.on('data', (d) => { st.buf += d; let nl; while ((nl = st.buf.indexOf('\n')) !== -1) { const line = st.buf.slice(0, nl); st.buf = st.buf.slice(nl + 1); if (!line.trim()) continue; let msg; try { msg = JSON.parse(line); } catch { process.stderr.write(`[pool] 워커가 JSON 아닌 줄을 stdout 에 썼다: ${line}\n`); continue; } results[msg.id] = msg.ok ? msg.result : { error: msg.err || 'unknown' }; done++; if (opts.onDone) opts.onDone(msg.id, results[msg.id], done, jobs.length); st.busy = -1; feed(st); } }); kid.on('exit', (code) => { if (st.busy >= 0) { // 죽으면서 물고 간 작업은 실패로 적는다 results[st.busy] = { error: `worker exited (${code})` }; done++; st.busy = -1; } st.dead = true; // 살아 있는 워커가 하나도 없는데 작업이 남았으면 더 못 돈다 — 남은 것은 null 로 끝난다. if (kids.every(k => k.dead) && cursor < jobs.length) { for (let i = cursor; i < jobs.length; i++) { results[i] = { error: 'no worker' }; done++; } cursor = jobs.length; } settleMaybe(); }); return st; }; let resolveAll; const all = new Promise((res) => { resolveAll = res; }); const settleMaybe = () => { if (done >= jobs.length) { for (const k of kids) if (!k.dead) k.kid.stdin.end(); resolveAll(results); } }; const feed = (st) => { if (st.dead) return; if (cursor >= jobs.length) { st.kid.stdin.end(); settleMaybe(); return; } const i = cursor++; st.busy = i; st.kid.stdin.write(JSON.stringify({ id: i, job: jobs[i] }) + '\n'); }; for (let k = 0; k < N; k++) kids.push(startKid()); for (const st of kids) feed(st); const out = await all; return out; } /* 워커 쪽 헬퍼 — stdin 의 JSON 줄을 받아 handler 에 넘기고 결과를 한 줄로 돌려준다. handler 가 던지면 그 작업만 실패로 돌아가고 워커는 계속 산다(한 슬롯의 버그가 나머지 31개의 측정을 못 지우게). */ export function serveWorker(handler) { let buf = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', async (d) => { buf += d; let nl; const lines = []; while ((nl = buf.indexOf('\n')) !== -1) { lines.push(buf.slice(0, nl)); buf = buf.slice(nl + 1); } for (const line of lines) { if (!line.trim()) continue; const { id, job } = JSON.parse(line); try { const result = await handler(job); process.stdout.write(JSON.stringify({ id, ok: true, result }) + '\n'); } catch (e) { process.stdout.write(JSON.stringify({ id, ok: false, err: (e && e.stack) || String(e) }) + '\n'); } } }); process.stdin.on('end', () => process.exit(0)); }