API KO - k-p2p-lab/kpl-v3 GitHub Wiki

REST API

Wiki home โ€บ ํ•œ๊ตญ์–ด ๋ฌธ์„œ ยท ์˜์–ด ์›๋ฌธ

๋ฌธ์„œ ์•ˆ๋‚ด ยท ์†Œ์Šค ์ €์žฅ์†Œ

Controller์˜ ๋Œ€์‹œ๋ณด๋“œ์™€ ์šด์˜ API๋Š” ๋กœ๊ทธ์ธ ์„ธ์…˜์œผ๋กœ ๋ณดํ˜ธํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ์ธ์ฆ์—์„œ ๊ณต๊ฐœ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฒฝ๋กœ์™€ ๋‚ด๋ถ€ ์„œ๋น„์Šค ์ธ์ฆ์„ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

Controller๋Š” ์›น ์ ‘๊ทผ๊ณผ ์ธ์ฆ ์‹œ๋„๋ฅผ <data-dir>/logs/access.jsonl, auth.jsonl์— ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ๋น„๋ฐ€๋ฒˆํ˜ธยท์ฟ ํ‚คยท์ธ์ฆ ํ—ค๋”ยท์ฟผ๋ฆฌ๋Š” ๊ธฐ๋กํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•„๋“œยทํšŒ์ „ยท์กฐํšŒ ๋ฐฉ๋ฒ•์€ ์›น ์ ‘๊ทผยท์ธ์ฆ ๋กœ๊ทธ๋ฅผ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

Controller endpoint

Method Path ์„ค๋ช…
POST /api/v1/auth/login JSON ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•˜๊ณ  ์„ธ์…˜ ์ฟ ํ‚ค ๋ฐœ๊ธ‰
GET /api/v1/auth/session ํ˜„์žฌ ์‚ฌ์šฉ์žยท์„ธ์…˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ
POST /api/v1/auth/logout ํ˜„์žฌ ์„ธ์…˜๊ณผ SSE ์—ฐ๊ฒฐ ์ข…๋ฃŒ
GET /metrics Controller์™€ ์‹คํ—˜ metric์˜ Prometheus exposition
GET /api/v1/health Controller ์ƒํƒœ์™€ Peer ์‹œ๊ณ„ ์ธก์ •์— ์“ฐ๋Š” ํ˜„์žฌ UTC ์‹œ๊ฐ
GET /api/v1/ui-config ๋Œ€์‹œ๋ณด๋“œ ๋ฉ”๋‰ด์— ์‚ฌ์šฉํ•˜๋Š” PrometheusยทGrafana ๊ฒŒ์‹œ ํฌํŠธ
GET /api/v1/prometheus/controller-targets Controller ๊ณต๊ฐœ metrics ์ฃผ์†Œ์˜ Prometheus HTTP service-discovery group
GET /api/v1/prometheus/agent-targets metrics URL์„ ์•Œ๋ฆฐ online Agent์˜ Prometheus HTTP service-discovery group
GET /api/v1/snapshot ๋…ธ๋“œ peerScores๋ฅผ ํฌํ•จํ•œ ๋Œ€์‹œ๋ณด๋“œ ์ „์ฒด snapshot
GET /api/v1/agents Agent ์ƒํƒœ
POST /api/v1/agents/refresh ๋“ฑ๋ก AgentยทPeer ์ƒํƒœ ์กฐํšŒ; ?discover=true๋กœ ๋ˆ„๋ฝ๋œ Swarm Agent๋„ ํƒ์ƒ‰; ์‹คํŒจ ์ •๋ณด ๋ฐ˜ํ™˜
PUT /api/v1/agents/{agentID}/capacity {capacity: 100}์œผ๋กœ Agent๋ณ„ ์˜ˆ์™ธ๊ฐ’ ์ง€์ •, {capacity: null}๋กœ CLI ๊ธฐ๋ณธ๊ฐ’ ๋ณต์›
GET /api/v1/nodes inspection์œผ๋กœ ์ˆ˜์ง‘ํ•œ peerScores๋ฅผ ํฌํ•จํ•œ Peer ์ƒํƒœ
GET /api/v1/network peerScores๊ฐ€ ํฌํ•จ๋œ Peer, ์—ฐ๊ฒฐ edge์™€ ์ „ํŒŒ ์ง€ํ‘œ
GET /api/v1/bootstrap?runId={runId} ํ•„์ˆ˜ run ID์— ์†ํ•œ ์ค€๋น„ ์ƒํƒœ์˜ bootstrap peer๋งŒ ๋ฐ˜ํ™˜
GET /api/v1/discovery?runId={runId}&topic={topic}&requesterNodeId={nodeId} ๋™์ผ runยท์ •ํ™•ํ•œ topic์—์„œ readyยทonline ์ƒํƒœ์ธ PubSub transport ํ›„๋ณด
GET /api/v1/events ์ตœ๊ทผ trace event
GET /api/v1/stream peerScores๋ฅผ ํฌํ•จํ•œ ์‹ค์‹œ๊ฐ„ snapshot SSE
GET /api/v1/experiments ์‹คํ—˜ ์ƒํƒœ์™€ activeJobs, completedJobs, failedJobs, canceledJobs counter
GET / POST /api/v1/scenarios ์‹œ๋‚˜๋ฆฌ์˜ค ์š”์•ฝ ๋ชฉ๋ก ์กฐํšŒ ๋˜๋Š” ๊ฒ€์ฆํ•œ {name, yaml} ์ €์žฅ
POST /api/v1/scenarios/validate ์›๋ณธ YAML์„ ์ €์žฅยท์‹คํ–‰ ์—†์ด ๊ฒ€์ฆ. ๋กœ๊ทธ์ธ ํ•„์š”
GET / PUT / DELETE /api/v1/scenarios/{id} ์ €์žฅ ์‹œ๋‚˜๋ฆฌ์˜ค ํ•˜๋‚˜๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ, ๊ฐฑ์‹  ๋˜๋Š” ์‚ญ์ œ
GET / POST /api/v1/library-groups ์‹œ๋‚˜๋ฆฌ์˜คยท๊ฒฐ๊ณผ๊ฐ€ ๊ณต์œ ํ•˜๋Š” ๊ทธ๋ฃน ์กฐํšŒ ๋˜๋Š” ๊ทธ๋ฃน ์ƒ์„ฑ
PUT /api/v1/library-groups/members ์„ ํƒํ•œ ์‹œ๋‚˜๋ฆฌ์˜คยท๊ฒฐ๊ณผ ๋ฌถ์Œ์„ ํ•œ ๊ทธ๋ฃน ๋˜๋Š” Ungrouped๋กœ ์ด๋™
PATCH / DELETE /api/v1/library-groups/{id} ์‹œ๋‚˜๋ฆฌ์˜คยท๊ฒฐ๊ณผ๋ฅผ ๋ณด์กดํ•˜๋ฉด์„œ ๊ทธ๋ฃน ์ด๋ฆ„ ๋ณ€๊ฒฝ ๋˜๋Š” ๊ทธ๋ฃน ์‚ญ์ œ
GET /api/v1/result-storage ๋กœ์ปฌ ์—ฌ์œ  ๊ณต๊ฐ„ยท์‹œ์ž‘ ๊ธฐ์ค€๊ณผ ์บ์‹œ๋œ NAS ๋ณด๊ด€ ์ƒํƒœ. ์š”์ฒญ ์ค‘ NAS๋ฅผ ์กฐํšŒํ•˜์ง€ ์•Š์Œ ์ €์žฅ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ๋กœ์ปฌ ๊ฒฐ๊ณผ ๋ชฉ๋ก์„ ๊ฐฑ์‹ ํ•  ์ˆ˜ ์žˆ๋Š” resultsRevision ๋ฌธ์ž์—ด์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•˜์ง€ ์•Š๊ณ  ํ† ํฐ์œผ๋กœ ๋น„๊ตํ•˜์‹ญ์‹œ์˜ค.
GET /api/v1/results ์ด์ „ Controller ์‹คํ–‰์—์„œ ์ €์žฅํ•œ ์‹คํ—˜์„ ํฌํ•จํ•˜๋Š” ๊ฒฐ๊ณผ ๋ชฉ๋ก
GET / PUT /api/v1/results/{id}/note ์‹คํ–‰๋ณ„ ๊ณต์œ  ๋ฉ”๋ชจ ์กฐํšŒยท์ €์žฅ. revision์œผ๋กœ ๋™์‹œ ์ˆ˜์ • ์ถฉ๋Œ ๊ฐ์ง€
GET / PUT /api/v1/result-batches/{batchId}/note ๊ฐœ๋ณ„ Run ๋ฉ”๋ชจ์™€ ๋…๋ฆฝ์ ์ธ ๊ทธ๋ฃน ๋ฉ”๋ชจ ์กฐํšŒยท์ €์žฅ. revision์œผ๋กœ ๋™์‹œ ์ˆ˜์ • ์ถฉ๋Œ ๊ฐ์ง€
DELETE /api/v1/results/{id} ๋น„ํ™œ์„ฑ ์ €์žฅ ๊ฒฐ๊ณผ ์‚ญ์ œ. ์ง„ํ–‰ ์ค‘ ๋ฐฐ์น˜ยท๋‹ค์šด๋กœ๋“œ ๋ณดํ˜ธ
DELETE /api/v1/result-batches/{batchId} ๊ทธ๋ฃน์˜ ๋ชจ๋“  ์ €์žฅ run๊ณผ ํ‰๊ท  ๋ถ„์„ ์‚ญ์ œ. ์ „์ฒด ๊ตฌ์„ฑ์›์˜ ํ™œ์„ฑ ์ƒํƒœยท๋‹ค์šด๋กœ๋“œ๋ฅผ ๋จผ์ € ๊ฒ€์‚ฌ
POST /api/v1/result-batches/{batchId}/resume ์‹คํŒจ ๋˜๋Š” ์‹œ์ž‘ ํ›„ interrupted ํšŒ์ฐจ๋ฅผ ์ œ์™ธํ•˜๊ณ  ๋ฏธ์‹œ์ž‘ ํšŒ์ฐจ๋งŒ ์žฌ๊ฐœ. ๋ฐฐ์น˜ยทํšŒ์ฐจ ID ์œ ์ง€. ์ฒซ queued run๊ณผ 202, ์ง„ํ–‰ ์ค‘ยท์žฌ๊ฐœ ๋ถˆ๊ฐ€ ์‹œ 409, ์—†๋Š” ๋ฐฐ์น˜๋ฉด 404. ๋กœ๊ทธ์ธ ์„ธ์…˜๊ณผ X-KPL-Request: dashboard ํ•„์š”.
POST /api/v1/result-batches/{batchId}/retry ์ด์ „ Peer๋ฅผ ์ •๋ฆฌํ•œ ๋’ค ์ €์žฅ๋œ ๋ฏธ์™„๋ฃŒ ํšŒ์ฐจ๋ฅผ ์ฒซ phase๋ถ€ํ„ฐ ์žฌ์‹คํ–‰. ์™„๋ฃŒ ํšŒ์ฐจยท๋ฐฐ์น˜ IDยทํšŒ์ฐจ ๋ฒˆํ˜ธยทseed ์œ ์ง€, ์ƒˆ run ID ์‚ฌ์šฉ. ์‹คํŒจยท์ค‘๋‹จยท์ทจ์†Œ, ๋‹จ์ผ ์‹คํ—˜๊ณผ ๋งˆ์ง€๋ง‰ ํšŒ์ฐจ ์‹คํŒจ ํฌํ•จ. ์ฒซ queued run๊ณผ 202, ์ง„ํ–‰ ์ค‘ยท์žฌ๊ฐœ ๋ถˆ๊ฐ€ ์‹œ 409, ์—†๋Š” ๋ฐฐ์น˜๋ฉด 404. ๋กœ๊ทธ์ธ ์„ธ์…˜๊ณผ X-KPL-Request: dashboard ํ•„์š”.
POST /api/v1/result-batches/{batchId}/append ์™„๋ฃŒ ๊ทธ๋ฃน์— {additionalRuns, expectedRepetitions}๋กœ ํšŒ์ฐจ ์ถ”๊ฐ€. ์ฒซ ์ƒˆ queued run๊ณผ 202 ๋ฐ˜ํ™˜
GET /api/v1/experiments/{id}/analysis ์ €์žฅ ์ด๋ฒคํŠธยท๊ด€์ธก์น˜๋ฅผ ๋ถ„์„ํ•œ ๊ทธ๋ž˜ํ”„ ๋ฐ์ดํ„ฐ์™€ ์ง‘๊ณ„ JSON
GET / POST /api/v1/analysis-jobs/{id} ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ถ„์„ ์ƒํƒœ ์กฐํšŒ / ์ ‘์ˆ˜. ์ค‘๋ณต ์š”์ฒญ ์žฌ์‚ฌ์šฉ, ?refresh=1๋กœ ์ƒˆ snapshot ๋ถ„์„
GET / HEAD /api/v1/analysis-jobs/{id}/result?jobId={jobId} ์„œ๋ฒ„์— ๋ณด๊ด€๋œ ์™„๋ฃŒ ๋ถ„์„ JSON ๋‹ค์šด๋กœ๋“œ. ์ž‘์—… ๋ฏธ์™„๋ฃŒยท๋‹ค๋ฅธ attempt๋Š” 409
GET / HEAD /api/v1/analysis-jobs/{id}/summary?jobId={jobId} ๋น„๊ต์šฉ ๊ฒฝ๋Ÿ‰ ๋ถ„์„ JSON. ๋ฉ”์‹œ์ง€๋ณ„ ๊ฒฝ๋กœ์™€ ์›๋ณธ ์‹œ๊ณ„์—ด์„ ์ œ์™ธํ•˜๊ณ  ์ง‘๊ณ„ยท๋ถ„ํฌยท์ ํ•ฉ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜
GET / POST /api/v1/batch-analysis-jobs/{batchId} ๋™์ผ ๋ฐ˜๋ณต ์‹คํ—˜์˜ ํ†ตํ•ฉ ํ‰๊ท  ์ž‘์—… ์ƒํƒœ ์กฐํšŒ / ์‹œ์ž‘. ?refresh=1์€ ์ƒˆ ๋กœ๊ทธ ๊ฒฝ๊ณ„๋กœ ๊ฐฑ์‹ 
GET / HEAD /api/v1/batch-analysis-jobs/{batchId}/result?jobId={jobId} ์ €์žฅ๋œ ํ†ตํ•ฉ ํ‰๊ท ๊ณผ ๊ฐœ์š”์šฉ run๋ณ„ ์ž…๋ ฅ ๋‹ค์šด๋กœ๋“œ
POST /api/v1/downloads ์„ ํƒํ•œ ์‹œ๋‚˜๋ฆฌ์˜คยทRun์˜ ZIP ๋‹ค์šด๋กœ๋“œ ์ค€๋น„
GET /api/v1/downloads/{ticket} ์ธ์ฆ ํ›„ ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ์žˆ๋Š” ์ผํšŒ์šฉ ํ‹ฐ์ผ“์„ ์†Œ๋ชจํ•˜๊ณ  ZIP ์ „์†ก
GET / HEAD /api/v1/experiments/{id}/download ์‹œ๋‚˜๋ฆฌ์˜คยท๋ฉ”ํƒ€๋ฐ์ดํ„ฐยท์ด๋ฒคํŠธยท์„ ํƒ์  ๊ด€์ธก ํŒŒ์ผยทํŒŒ์ƒ ์ง€ํ‘œ๋ฅผ ZIP์œผ๋กœ ๋‹ค์šด๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ์‘๋‹ต ๋ณธ๋ฌธ ์—†์ด ํฌ๊ธฐ ๊ณ„์‚ฐ
POST /api/v1/experiments YAML 1ํšŒ ์‹คํ–‰ ๋˜๋Š” JSON {scenario, repetitions}๋กœ 1~100ํšŒ ์ˆœ์ฐจ ์‹คํ–‰
POST /api/v1/experiments/{id}/stop X-KPL-Execution์œผ๋กœ ์ง€์ •ํ•œ ์‹คํ–‰์„ ์ทจ์†Œํ•œ ๋’ค ์ œํ•œ ์‹œ๊ฐ„ ๋‚ด job ์ข…๋ฃŒ์™€ generation-fenced Peer cleanup ์ˆ˜ํ–‰. ๋ˆ„๋ฝยท๊ณผ๊ฑฐ ์‹คํ–‰ ID๋Š” 409

/api/v1/bootstrap์˜ runId query parameter๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. registry๋Š” ํ•ด๋‹น run์—์„œ ์ค€๋น„ ์ƒํƒœ์ด๊ณ  ์œ ํšจํ•œ identity์™€ address ์ •๋ณด๊ฐ€ ์žˆ๋Š” boot ๋…ธ๋“œ๋งŒ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ๋™์‹œ์— ์‹คํ–‰๋˜๋Š” ์‹คํ—˜๋ผ๋ฆฌ bootstrap peer๋ฅผ ๋ฐœ๊ฒฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. /api/v1/discovery์—๋Š” ํ‘œ์‹œ๋œ query ์„ธ ๊ฐœ๊ฐ€ ๋ชจ๋‘ ํ•„์š”ํ•˜๋ฉฐ ์š”์ฒญ์ž ์ž์‹ ์„ ์ œ์™ธํ•˜๊ณ  ์‹ค์ œ ์ „๋‹ฌยทmesh ๊ฒฐ๊ณผ๊ฐ€ ์•„๋‹Œ ์„ค์ •์ƒ topic ์ฐธ๊ฐ€์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. /api/v1/prometheus/agent-targets๋Š” ๊ธฐ๋ณธ Swarm Prometheus ์„ค์ •์ด ์‚ฌ์šฉํ•˜๋Š” ์ฝ๊ธฐ ์ „์šฉ ์šด์˜ endpoint์ด๋ฉฐ offline Agent์™€ ์œ ํšจํ•œ metrics URL์ด ์—†๋Š” Agent๋Š” ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค.

Bootstrap ์‘๋‹ต์€ {nodeId, peerId, addresses} ํ•ญ๋ชฉ ๋ฐฐ์—ด์ด๋ฉฐ ๋น„์–ด ์žˆ์œผ๋ฉด null์ž…๋‹ˆ๋‹ค. Discovery์™€ ๋‹ฌ๋ฆฌ bootstrap์€ Agent online ์ƒํƒœ๋กœ ํ•„ํ„ฐ๋งํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Discovery๋Š” ํ›„๋ณด๊ฐ€ ์—†์„ ๋•Œ ๋นˆ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ๊ฐ ํ•ญ๋ชฉ์—๋Š” subscribed flag๋„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์กฐ๊ฑด์— ๋งž๋Š” ์ „์ฒด ํ›„๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์š”์ฒญํ•˜๋Š” Peer๊ฐ€ ํ† ํด๋กœ์ง€์— ์„ค๋ช…ํ•œ rendezvous ์ˆœ์œ„, connection budget๊ณผ retry๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

/api/v1/prometheus/controller-targets๋Š” Controller์˜ --metrics-url (KPL_CONTROLLER_METRICS_URL)์„ ๊ด‘๊ณ ํ•˜๋ฉฐ ๋ฏธ์„ค์ • ์‹œ []๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ธ์ฆ ์—†์ด ์กฐํšŒํ•˜๋Š” ์ฝ๊ธฐ ์ „์šฉ endpoint์ž…๋‹ˆ๋‹ค. URL์€ HTTP(S)์˜ /metrics ๊ฒฝ๋กœ์—ฌ์•ผ ํ•˜๋ฉฐ ์ธ์ฆ์ •๋ณดยทqueryยทfragmentยทloopback/unspecified ์ฃผ์†Œ๋ฅผ ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Swarm helper๊ฐ€ control ๋…ธ๋“œ ์ฃผ์†Œ์™€ KPL_HTTP_PORT๋กœ ์ž๋™ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

Agent ์ƒˆ๋กœ๊ณ ์นจ

POST /api/v1/agents/refresh๋Š” ๋กœ๊ทธ์ธ ์„ธ์…˜๊ณผ X-KPL-Request: dashboard๋ฅผ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค. Offline์„ ํฌํ•จํ•œ ๋“ฑ๋ก Agent์—์„œ ์ตœ์‹  AgentยทPeer ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ?discover=true๋ฅผ ๋ถ™์ด๋ฉด ์‹คํ–‰ ์ค‘์ธ Swarm Agent task๋„ ํƒ์ƒ‰ํ•˜์—ฌ ๋ˆ„๋ฝ๋œ ๋“ฑ๋ก์„ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค. Controller์˜ --agent-discovery-dns / KPL_AGENT_DISCOVERY_DNS ์ด๋ฆ„์„ ์กฐํšŒํ•˜๊ณ  ๊ฐ task IP์˜ 8090 ํฌํŠธ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. Swarm stack์€ tasks.<stack>_agent๋ฅผ ์ž๋™ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. Controller์— Docker socket์„ ๋งˆ์šดํŠธํ•˜๊ฑฐ๋‚˜ task๋ฅผ ์žฌ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ Peer๋ฅผ ์ •๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ตœ๋Œ€ 4๊ฐœ๋ฅผ ๋™์‹œ์— ํ™•์ธํ•˜๋ฉฐ ์ „์ฒด ์ œํ•œ ์‹œ๊ฐ„์€ 15์ดˆ์ž…๋‹ˆ๋‹ค. DNS ์กฐํšŒ์—๋Š” 3์ดˆ ์ œํ•œ, ํƒ์ƒ‰์—๋Š” ์ค‘๋ณต ์ œ๊ฑฐํ•œ task ์ฃผ์†Œ 256๊ฐœ ์ œํ•œ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. DNS๊ฐ€ ์‹คํŒจํ•ด๋„ ๊ธฐ์กด ๋“ฑ๋ก ์ฃผ์†Œ๋Š” ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ๋ฏธ๋“ฑ๋กยท๊ต์ฒด๋œ Agent์—๋Š” ์ธ์ฆ๋œ POST /api/v1/registration/refresh๋ฅผ ์š”์ฒญํ•˜๋ฉฐ, Agent๊ฐ€ ์ž์‹ ์˜ Controller URL๊ณผ ์„œ๋น„์Šค ํ‚ค๋กœ ์ด Controller์— ๋‹ค์‹œ ๋“ฑ๋กํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฏธ๋“ฑ๋ก Agent์˜ ์ƒํƒœ ์‘๋‹ต๋งŒ์œผ๋กœ ๋“ฑ๋ก์„ ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด IDยท์ธ์Šคํ„ด์Šค ์„ธ๋Œ€ ๊ฒ€์‚ฌ์™€ ์ €์žฅ๋œ ์šฉ๋Ÿ‰ยท๋น„ํ™œ์„ฑํ™” ์„ค์ •์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ๋“ฑ๋ก๋œ ์ธ์Šคํ„ด์Šค๋Š” ์ด๋ ฅ ์žฌ์ „์†ก์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜์ง€ ์•Š๊ณ  ์ƒํƒœ๋งŒ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

์‘๋‹ต์€ 200๊ณผ {agents, requested, refreshed, failures}์ž…๋‹ˆ๋‹ค. ํƒ์ƒ‰ ์‹œ discovery: {enabled, addresses, added, error?}๋„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. addresses๋Š” ์ œํ•œ ์ ์šฉ ์ „ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•œ task ์ฃผ์†Œ ์ˆ˜์ด๋ฉฐ, added๋Š” ์š”์ฒญ ์‹œ์ž‘ ๋‹น์‹œ ๋ชฉ๋ก์— ์—†๋˜ ID ์ค‘ ๊ฐฑ์‹ ์— ์„ฑ๊ณตํ•œ ์ˆ˜์ž…๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ์ฃผ์†Œ์—์„œ ๊ฐ™์€ Agent๋ฅผ ํ™•์ธํ•ด๋„ ์„ฑ๊ณต Agent์™€ ์‹คํŒจํ•œ ์•Œ๋ ค์ง„ ID๋Š” ๊ฐ๊ฐ ํ•œ ๋ฒˆ๋งŒ ์ง‘๊ณ„ํ•ฉ๋‹ˆ๋‹ค. ์‹๋ณ„ํ•˜์ง€ ๋ชปํ•œ task ์ฃผ์†Œ๋Š” ๊ฐœ๋ณ„ ์‹คํŒจ๋กœ ์ง‘๊ณ„ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ์‹คํŒจ์—๋Š” {id, name, error}๊ฐ€ ์žˆ์œผ๋ฉฐ, ๋ฏธ์‹๋ณ„ ์ฃผ์†Œ๋Š” ๋นˆ id์™€ URL์„ name์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. DNSยท์„ค์ • ์˜ค๋ฅ˜๋Š” ์„ฑ๊ณตํ•œ ๊ฐฑ์‹ ์„ ์œ ์ง€ํ•œ ์ฑ„ discovery.error์— ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ ์‹คํŒจ ์‹œ Agent ๊ธฐ๋ก์„ ์‚ญ์ œํ•˜์ง€ ์•Š์œผ๋ฉฐ Offline ํŒ์ •์—๋Š” ๊ธฐ์กด heartbeat ๋งŒ๋ฃŒ ๊ธฐ์ค€์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ƒˆ๋กœ๊ณ ์นจยทํƒ์ƒ‰์ด ์ด๋ฏธ ์ง„ํ–‰ ์ค‘์ด๋ฉด 409, ์ž˜๋ชป๋œ discover ๊ฐ’์€ 400์ž…๋‹ˆ๋‹ค. task ์ž์ฒด๊ฐ€ ์‹œ์ž‘๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ์ œ์–ด API๋ฅผ ์—ด์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ์—๋Š” SwarmยทDocker ์‹œ์ž‘ ์˜ค๋ฅ˜๋ฅผ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋“ฑ๋ก ๋ณต๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด Controller์™€ Agent ์ด๋ฏธ์ง€๋ฅผ ํ•จ๊ป˜ ๊ฐฑ์‹ ํ•˜๊ณ  stack ์„ค์ •์„ ์žฌ๋ฐฐํฌํ•˜์‹ญ์‹œ์˜ค.

Agent ์šฉ๋Ÿ‰ ์„ค์ •

PUT /api/v1/agents/{agentID}/capacity๋Š” ๋กœ๊ทธ์ธ ์„ธ์…˜๊ณผ X-KPL-Request: dashboard๋ฅผ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค. ํ•„์ˆ˜ capacity ํ•„๋“œ๋Š” ์–‘์˜ ์ •์ˆ˜ ๋˜๋Š” ์˜ˆ์™ธ๊ฐ’์„ ์‚ญ์ œํ•˜๋Š” null์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œยท์†Œ์ˆ˜ยท0 ์ดํ•˜ ๊ฐ’์€ 400์ž…๋‹ˆ๋‹ค. ๊ฐฑ์‹ ํ•œ Agent์™€ 200์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, ์—†๋Š” ID๋Š” 404, ์ด ๊ธฐ๋Šฅ ์ง€์›์„ ๋ณด๊ณ ํ•˜์ง€ ์•Š๋Š” Agent๋Š” 409์ž…๋‹ˆ๋‹ค. ๋“ฑ๋ก๋œ ์˜คํ”„๋ผ์ธ Agent๋„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €์žฅ ์‹คํŒจ ์‹œ ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ํ˜„์žฌ ์„ค์ •์€ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Agent ๋ ˆ์ฝ”๋“œ์—๋Š” defaultCapacity(์‹œ์ž‘ CLI ๊ฐ’), ์„ ํƒ์  capacityOverride(์ €์žฅ ์˜ˆ์™ธ๊ฐ’), capacityPending(์ ์šฉ ํ™•์ธ ๋Œ€๊ธฐ)์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. capacity๋Š” Controller๊ฐ€ ํ˜„์žฌ ๋ฐฐ์น˜์— ์‚ฌ์šฉํ•˜๋Š” ํ•œ๋„์ž…๋‹ˆ๋‹ค. ๋“ฑ๋กยทheartbeat ์‘๋‹ต์˜ X-KPL-Agent-Capacity, X-KPL-Agent-Capacity-Revision์œผ๋กœ ์„ค์ •์„ ์ „๋‹ฌํ•˜๋ฉฐ heartbeat ์‘๋‹ต์€ ๊ธฐ์กด 204๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. Agent๋Š” ์ƒ์„ฑ admission๊ณผ ๊ฐ™์€ ์ž ๊ธˆ ์•„๋ž˜ ์„ค์ •์„ ์ ์šฉํ•˜๊ณ  ๋‹ค์Œ ๋ณด๊ณ ์— ์‹ค์ œ capacity์™€ capacityRevision์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ํ˜„์žฌ ์„ค์ •์„ ํ™•์ธํ•œ ๋ณด๊ณ ๋งŒ ์šฉ๋Ÿ‰ ์ฆ๊ฐ€๋ฅผ ํ—ˆ์šฉํ•˜๋ฏ€๋กœ ์ง€์—ฐ๋œ ๋ณด๊ณ ๊ฐ€ ๊ฐ์†Œ ์„ค์ •์„ ์ทจ์†Œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Controller ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ agent-capacities.json์— ์˜ˆ์™ธ๊ฐ’์„ ์ €์žฅํ•˜๋ฉฐ ํŒŒ์ผ์ด ์†์ƒ๋˜๋ฉด ํ•œ๋„๋ฅผ ์ž„์˜ ๋ณต์›ํ•˜์ง€ ์•Š๊ณ  Controller ์‹œ์ž‘์„ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค.

Agent์˜ ๋กœ์ปฌ ์šฉ๋Ÿ‰ ์ œํ•œ ๋•Œ๋ฌธ์— create-node ์š”์ฒญ์„ ๊ฑฐ์ ˆํ•˜๋ฉด Peer๋ฅผ ๋งŒ๋“ค๊ธฐ ์ „์— 429๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Controller๋Š” ํ•ด๋‹น ์˜ˆ์•ฝ์„ ํ•ด์ œํ•˜๊ณ  ๊ธฐ์กด ์ž‘์—… ์ปจํ…์ŠคํŠธ ์•ˆ์—์„œ ๋ฐฐ์น˜๋ฅผ ๋Œ€๊ธฐยท์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ƒ์„ฑ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋Š” ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

PUT /api/v1/agents/{agentID}/enabled์— {"enabled": true} ๋˜๋Š” {"enabled": false}๋ฅผ ๋ณด๋‚ด ์ฐธ์—ฌ ์—ฌ๋ถ€๋ฅผ ์„ค์ •ํ•˜๋ฉฐ ๊ฐฑ์‹ ํ•œ Agent๋ฅผ 200์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋ˆ„๋ฝยทnullยทboolean ์ด์™ธ ๊ฐ’ยท์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ๋Š” 400, ์กด์žฌํ•˜์ง€ ์•Š๋Š” Agent๋Š” 404์ž…๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ๊ณผ X-KPL-Request: dashboard๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ œ์™ธ๋œ Agent๋Š” disabled: true๋กœ ํ‘œ์‹œํ•˜๋ฉฐ ์ƒ๋žต ๋˜๋Š” false๋Š” ํ™œ์„ฑ์ž…๋‹ˆ๋‹ค. online/offline ์—ฐ๊ฒฐ ์ƒํƒœ ๋ฐ capacity ์˜ˆ์™ธ ์„ค์ •๊ณผ ๋…๋ฆฝ์ ์ž…๋‹ˆ๋‹ค.

๋น„ํ™œ์„ฑ Agent๋„ ๋“ฑ๋ก๊ณผ ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜๋ฉฐ ๊ธฐ์กด Peer ์ž‘์—…ยท์ •๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์ƒˆ ๋ฐฐ์น˜ ๋ชจ๋“œ, ํ•˜๋“œ์›จ์–ด ํ•ฉ๊ณ„ยทgauge, Prometheus Agent target discovery, Dashboard ํ† ํด๋กœ์ง€์—์„œ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์ „์†กํ•œ ์ƒ์„ฑ ์š”์ฒญ๊ณผ ๊ธฐ์กด Peer๋Š” ๊ณ„์† ์‹คํ–‰๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋น„ํ™œ์„ฑํ™”๊ฐ€ ์ด๋ฅผ ์ข…๋ฃŒํ•˜๊ฑฐ๋‚˜ ์‹คํ—˜ ์ธก์ •๊ฐ’์„ ๋‹ค์‹œ ์“ฐ์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ๋น„ํ™œ์„ฑ Agent๋ฅผ ์ง€์ •ํ•œ explicit/single-Agent join์€ ์žฌํ™œ์„ฑํ™” ๋˜๋Š” ๊ธฐ์กด ์ทจ์†Œยท์ œํ•œ์‹œ๊ฐ„์„ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ „์ฒด REST ์ธ๋ฒคํ† ๋ฆฌ์—๋Š” ์†Œ์† Peer๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ Dashboard๋Š” ์„ค์ •์šฉ Agent ํ–‰์„ ๋‚จ๊ธฐ๊ณ  Peer์™€ ๊ฐ„์„ ๋งŒ ์ˆจ๊น๋‹ˆ๋‹ค. ID๋ณ„ ์„ค์ •์€ Controller ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ agent-availability.json์— ์ €์žฅํ•˜๊ณ  heartbeat๋กœ ๋ฎ์–ด์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ €์žฅ ์‹คํŒจ ์‹œ ์ด์ „ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ์„ ์ฝ์„ ์ˆ˜ ์—†์œผ๋ฉด ๋“ฑ๋ก๋œ Agent๋ฅผ ๋น„ํ™œ์„ฑ์œผ๋กœ ์œ ์ง€ํ•˜๋ฉฐ ํŒŒ์ผ ๋ณต๊ตฌ ํ›„ Controller๋ฅผ ์žฌ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Dashboard SSE

๋ธŒ๋ผ์šฐ์ €๋Š” GET /api/v1/stream?view=dashboard&topology=0์œผ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. Overview๋ฅผ ์ผœ๋ฉด topology=1๋กœ ์ƒˆ ์—ฐ๊ฒฐ์„ ์—ด๊ณ , ๋„๋ฉด topology=0์œผ๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ๋งˆ๋‹ค ์ฒซ ๋ฐ์ดํ„ฐ snapshot์€ ์„ ํƒํ•œ ๋Œ€์‹œ๋ณด๋“œ view์˜ ์ „์ฒด ๊ธฐ์ค€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์€ event: snapshot์ด๋ฉฐ, ์ด๋ฅผ ํ•œ ๋ฒˆ ๋ณด๋‚ธ ๋’ค, ๋ณ€๊ฒฝ์ด ์žˆ์œผ๋ฉด ์ตœ๋Œ€ ์ดˆ๋‹น 1ํšŒ event: snapshot_delta๋ฅผ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ์žฌ์ ‘์†์€ ํ•ญ์ƒ ์ „์ฒด ๋ฐ์ดํ„ฐ๋กœ ์‹œ์ž‘ํ•˜๋ฏ€๋กœ Last-Event-ID ์žฌ์ „์†ก์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์œ ํœด ์—ฐ๊ฒฐ๋„ ์‹œ๊ฐ„์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง€๋Š” ๊ฐ’์„ ๊ฐฑ์‹ ํ•˜๋ฉฐ, 15์ดˆ ๋™์•ˆ ๋ณ€๊ฒฝ์ด ์—†์œผ๋ฉด event: heartbeat, data: {}๋ฅผ ๋ณด๋ƒ…๋‹ˆ๋‹ค. SSE ์ฃผ์„๊ณผ ๋‹ฌ๋ฆฌ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด ์—ฐ๊ฒฐ ๋ฌด์‘๋‹ต ๊ฐ์ง€์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

topology=0์€ ์ƒ์„ธ ๋…ธ๋“œ projection๊ณผ ๊ฐ„์„  ๊ณ„์‚ฐ์„ ์ƒ๋žตํ•˜๋ฉฐ nodes: [], edges: []๋ฅผ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋‘ dashboard ๋ชจ๋“œ๋Š” ๋ชจ๋‘ topology: {included, total, ready, starting, other}๋ฅผ ํฌํ•จํ•˜๊ณ , ํ”ผ์–ด ์ˆ˜ ์ •์˜๋Š” ํ† ํด๋กœ์ง€ ์ˆ˜๋ช… ์ƒํƒœ ๊ทœ์น™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ๋น„ํ™œ์„ฑ Agent์™€ stoppingยทstoppedยทfailed Peer๋Š” ์ œ์™ธํ•˜๋ฉฐ ๋ ˆ์ด์–ดยทtopic ํ•„ํ„ฐ๋Š” ํ”ผ์–ด ์ˆ˜๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Agent, ์‹คํ—˜ ์ง„ํ–‰ ์ •๋ณด, ์ง€ํ‘œ์™€ ์ด๋ฒคํŠธ ์š”์•ฝ์€ ๋‘ ๋ชจ๋“œ ๋ชจ๋‘ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €๋Š” Overview๊ฐ€ Off์ด๊ฑฐ๋‚˜ ๋กœ๋”ฉ ์ค‘์ผ ๋•Œ Transport links๋ฅผ ๋ฏธ์ƒ์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. topology๋ฅผ ์ƒ๋žตํ•˜๋ฉด ๊ธฐ์กด dashboard ํด๋ผ์ด์–ธํŠธ๋ฅผ ์œ„ํ•ด ๊ทธ๋ž˜ํ”„๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

snapshot ๊ณ„์‚ฐ์€ raw view, ๊ทธ๋ž˜ํ”„๋ฅผ ํฌํ•จํ•˜๋Š” dashboard view, ๊ทธ๋ž˜ํ”„๋ฅผ ์ œ์™ธํ•˜๋Š” dashboard view๋ณ„๋กœ ์ตœ๋Œ€ ํ•˜๋‚˜์”ฉ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜ํ”„๋ฅผ ์ œ์™ธํ•˜๋Š” view๋Š” ์ง„ํ–‰ ์ค‘์ธ ๊ทธ๋ž˜ํ”„ ๊ณ„์‚ฐ์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ทจ์†Œ๋œ ์กฐํšŒ๋Š” ์ฆ‰์‹œ ๋Œ€๊ธฐ๋ฅผ ๋๋‚ด๊ณ  ์žฌ์ ‘์†์€ ํ•ด๋‹น ๋ชจ๋“œ์˜ ๊ธฐ์กด ๊ณ„์‚ฐ์„ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ๊ณ„์‚ฐ ์ค‘์—๋Š” ์ฒซ snapshot ์ด์ „์—๋„ 15์ดˆ๋งˆ๋‹ค heartbeat๋ฅผ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ๋Œ€๊ธฐ ์ค‘ heartbeat๋Š” 30์ดˆ ์—ฐ๊ฒฐ ๊ฐ์‹œ ํƒ€์ด๋จธ๋ฅผ ๋‹ค์‹œ ์„ค์ •ํ•˜์ง€๋งŒ Dashboard๋ฅผ Live๋กœ ํ‘œ์‹œํ•˜๊ฑฐ๋‚˜ ๊ธฐ์กด ํ™”๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๊ต์ฒดํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ์ง€ํ‘œ ์žฌ๊ตฌ์„ฑ์€ Run๋ณ„ ์ž ๊ธˆ์„ ์‚ฌ์šฉํ•˜๋ฉฐ Agent heartbeatยท๋ฐฐ์น˜๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” Controller ์ธ๋ฒคํ† ๋ฆฌ ์ž ๊ธˆ ๋ฐ–์—์„œ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ž˜ํ”„๋ฅผ ํฌํ•จํ•˜๋Š” view์˜ node metadata์—์„œ๋Š” ์˜ˆ์•ฝ ๊ณ„ํš ์ „์ฒด๊ฐ€ ๋ฐ˜๋ณต ์ „์†ก๋˜์ง€ ์•Š๋„๋ก networkRequested์™€ networkSchedule์„ ์ œ์™ธํ•˜๊ณ , ํ˜„์žฌ networkยท์ ์šฉ ์ƒํƒœยทrevision์€ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒด REST snapshot์—๋Š” ๋‘ ๊ณ„ํš์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ž˜ํ”„๋ฅผ ํฌํ•จํ•˜๋Š” ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ๋Š” ํ˜„์žฌ Peer(stoppingยทstoppedยทfailed ์ œ์™ธ), ์ „์ฒด ์‹คํ—˜ ์ง„ํ–‰ ์ •๋ณด, ํ† ํด๋กœ์ง€, ์ง€ํ‘œ, ์ตœ๊ทผ ์ด๋ฒคํŠธ ์š”์•ฝ 40๊ฐœ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ์š”์•ฝ์€ run/node ID, ์ข…๋ฅ˜, ์ƒ๋Œ€ Peer, ์‹œ๊ฐ, ์ง€์—ฐ๊ณผ ํ™”๋ฉด ํ‘œ์‹œ์šฉ ๋‹จ์ผ ๊ฐ’ latencyAvailable, direction, controlType, controlEntries, messageIdCount, peerExchangeCount๋งŒ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์›๋ณธ message-ID/cohort ๋ฐฐ์—ด๊ณผ ์ด๋ฒคํŠธ๋ณ„ bandwidth ํ‘œ๋ณธ์€ ๋ณด๋‚ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ „์ฒด trace ๊ธฐ๋ก์€ ๊ธฐ์กด snapshot API์™€ ์ €์žฅ๋œ ์‹คํ—˜ ๋‚ด๋ณด๋‚ด๊ธฐ์— ๋ณด์กด๋ฉ๋‹ˆ๋‹ค.

๋ณ€๊ฒฝ๋ถ„์—๋Š” ๋ฐ”๋€ ์˜์—ญ๋งŒ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. agents, nodes, experiments๋Š” ์„ ํƒ ํ•„๋“œ upsert(์ถ”๊ฐ€ยท๋ณ€๊ฒฝ๋œ ํ•ญ๋ชฉ์˜ ์ „์ฒด ๋‚ด์šฉ, id ๊ธฐ์ค€), remove(์‚ญ์ œ ID), order(์ˆœ์„œ๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ „์ฒด ID ์ˆœ์„œ)๋ฅผ ๊ฐ–๋Š” ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. ์‚ญ์ œ, upsert, ์ˆœ์„œ ์ ์šฉ ์ˆœ์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. edges, events, metrics, topology๋Š” ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ๋นˆ ๋ฐฐ์—ดยทnull์„ ํฌํ•จํ•ด ํ•ด๋‹น ์˜์—ญ ์ „์ฒด๋ฅผ ๊ต์ฒดํ•˜๋ฉฐ, ๊ฐฑ์‹  ์‹œ generatedAt๋„ ๋ฐ”๋€๋‹ˆ๋‹ค. ์—ฐ๊ฒฐ๋งˆ๋‹ค ๋งˆ์ง€๋ง‰์œผ๋กœ ์ „๋‹ฌํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋น„๊ตํ•˜๋ฏ€๋กœ ์ค‘๊ฐ„ ๊ฐฑ์‹ ์„ ๊ฑด๋„ˆ๋›ฐ์–ด๋„ ๋ณ€๊ฒฝ์ด ์œ ์‹ค๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

view ์—†๋Š” ๊ธฐ์กด GET /api/v1/stream์€ ๋‹ค๋ฅธ ํด๋ผ์ด์–ธํŠธ์™€์˜ ํ˜ธํ™˜์„ ์œ„ํ•ด ์›๋ณธ ์ตœ๊ทผ ์ด๋ฒคํŠธ ์ƒ์„ธ๋ฅผ ํฌํ•จํ•œ ์ „์ฒด snapshot ํ˜•์‹์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์•Œ ์ˆ˜ ์—†๋Š” view ๋˜๋Š” ๋น„์–ด ์žˆ์ง€ ์•Š์€ 0/1 ์ด์™ธ์˜ topology ๊ฐ’์€ 400์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋น„์–ด ์žˆ์ง€ ์•Š์€ topology ์„ ํƒ ๊ฐ’์—๋Š” view=dashboard๋„ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ชจ๋“œ์— ๊ฐ™์€ ์„ธ์…˜ ์ธ์ฆ์„ ์ ์šฉํ•˜๊ณ  ๋กœ๊ทธ์•„์›ƒยท์„ธ์…˜ ๋งŒ๋ฃŒยทController ์ข…๋ฃŒ ์‹œ ์—ฐ๊ฒฐ์„ ๋‹ซ์Šต๋‹ˆ๋‹ค. ์‘๋‹ต์€ ์บ์‹œ๋ฅผ ๊ธˆ์ง€ํ•˜๊ณ  reverse proxy์— ๋ฒ„ํผ๋ง ๋น„ํ™œ์„ฑํ™”๋ฅผ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค(X-Accel-Buffering: no).

๋Œ€์‹œ๋ณด๋“œ๋Š” ๋ชจ๋ฐ”์ผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํƒญ ๋“ฑ ๋ฌธ์„œ๊ฐ€ ์ˆจ๊ฒจ์ง€๊ฑฐ๋‚˜ ํŽ˜์ด์ง€๋ฅผ ๋– ๋‚  ๋•Œ SSE๋ฅผ ๋‹ซ์Šต๋‹ˆ๋‹ค. ๋Œ์•„์˜ค๋ฉด ์—ฐ๊ฒฐ ํ•˜๋‚˜๋ฅผ ๋‹ค์‹œ ์—ด์–ด ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. ํ™”๋ฉด์ด ๋ณด์ด๋Š” ๋™์•ˆ ์ŠคํŠธ๋ฆผ์€ ๊ณ„์† ์—ด๋ ค ์žˆ์œผ๋ฏ€๋กœ ๋ˆ„์  ์ˆ˜์‹  ๋ฐ”์ดํŠธ๋Š” ์ •์  ํŽ˜์ด์ง€ ๋‹ค์šด๋กœ๋“œ ํฌ๊ธฐ์™€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

์ตœ์ดˆ snapshot์€ 30์ดˆ, ์ดํ›„ snapshotยท๋ณ€๊ฒฝ๋ถ„ยทheartbeat ์‚ฌ์ด๋Š” 45์ดˆ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ด๋ฅผ ๋„˜์œผ๋ฉด ๋ฉˆ์ถ˜ ์—ฐ๊ฒฐ์„ ๋‹ซ๊ณ  SSE ์˜ค๋ฅ˜์™€ ๊ฐ™์€ ๋ณต๊ตฌ ๊ฒฝ๋กœ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์„ธ์…˜ ํ™•์ธ์€ 8์ดˆ ๋’ค ์ทจ์†Œํ•˜๋ฉฐ, ์‹คํŒจยท์‹œ๊ฐ„ ์ดˆ๊ณผ์—ฌ๋„ ๋‹ค์Œ ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. 401์€ Login required ์•ˆ๋‚ด ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ณต ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ์€ ๋ฌด์ž‘์œ„ ํŽธ์ฐจ๋ฅผ ํฌํ•จํ•ด ์•ฝ 2์ดˆ์—์„œ ์ตœ๋Œ€ 30์ดˆ๊นŒ์ง€ ๋Š˜์–ด๋‚˜๊ณ , ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์œผ๋ฉด ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ € online ์ด๋ฒคํŠธ๋Š” ์˜ค๋ž˜๋œ ์—ฐ๊ฒฐยท์ธ์ฆ ํ™•์ธ์„ ์ทจ์†Œํ•˜๊ณ  ํ™”๋ฉด์ด ๋ณด์ด๋ฉด ๋ฐ”๋กœ ์žฌ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ํ™”๋ฉด์„ ์ˆจ๊ธฐ๊ฑฐ๋‚˜ ๋– ๋‚˜๋ฉด SSE์™€ ์ง„ํ–‰ ์ค‘์ธ ์„ธ์…˜ ํ™•์ธ์„ ํ•จ๊ป˜ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์˜ 10์ดˆ ์“ฐ๊ธฐ ์ œํ•œ์‹œ๊ฐ„์€ ๊ฐ ์ „์†กยทflush ์ค‘์—๋งŒ ์ ์šฉํ•˜๊ณ  ์งํ›„ ํ•ด์ œํ•˜๋ฏ€๋กœ HTTP/2 ์œ ํœด ์—ฐ๊ฒฐ์ด ๋‹ค์Œ heartbeat ์ „์— ๋Š๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Agent ๋ฆฌ์†Œ์Šค ๋‚ด๋ณด๋‚ด๊ธฐ

Agent ๋“ฑ๋กยทheartbeatยท์ƒํƒœยท๋Œ€์‹œ๋ณด๋“œ ๊ธฐ๋ก์—๋Š” ์„ ํƒ์ ์œผ๋กœ resources๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ํ•„๋“œ๋Š” sampledAt, ์›์‹œ ์‚ฌ์šฉ ์ฝ”์–ด ์ˆ˜ cpuCores, ํ˜ธ์ŠคํŠธ ๋…ผ๋ฆฌ CPU ์ˆ˜ cpuCapacityCores, memoryUsageBytes, memoryWorkingSetBytes, containers, measuredContainers, complete, ์„ ํƒ์  error์ž…๋‹ˆ๋‹ค. ๋ฒ”์œ„๋Š” Agent์™€ ์†Œ์† ์‹คํ–‰ ์ค‘ Peer์ž…๋‹ˆ๋‹ค. ๋ถ€๋ถ„ ์ธก์ •์€ complete: false์™€ ํ•จ๊ป˜ ์ธก์ •๋œ ์ปจํ…Œ์ด๋„ˆ ํ•ฉ๊ณ„๋ฅผ ๋ณด์กดํ•˜๋ฉฐ ์ธก์ • 0๊ฐœ๋Š” ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค. Controller๋Š” ํ‘œ๋ณธ ๊ฒฝ๊ณผ ์‹œ๊ฐ„์„ ์ •๊ทœํ™”ํ•˜๊ณ  Offline ๋˜๋Š” 30์ดˆ๋ณด๋‹ค ์˜ค๋ž˜๋œ ํ‘œ๋ณธ์€ ํ˜„์žฌ ํ•ฉ๊ณ„์—์„œ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ํ˜ธ์ŠคํŠธ CPU ๋ฐฑ๋ถ„์œจ์€ cpuCores / cpuCapacityCores ร— 100์ด๋ฉฐ CPU ์ˆ˜ ๋ฏธ์ƒ์ผ ๋•Œ ๋ฐฑ๋ถ„์œจ๋งŒ ์ƒ๋žตํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ๋Š” ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

์ƒˆ Agent ํ‘œ๋ณธ์€ ์„ ํƒ์  resources.agent, resources.peers ๊ฐ์ฒด๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๊ฐ์ฒด๋Š” cpuCores, memoryUsageBytes, memoryWorkingSetBytes, containers, measuredContainers, complete๋ฅผ ๊ฐ€์ง€๋ฉฐ ์ƒ์œ„ ํ‘œ๋ณธ ์‹œ๊ฐ๊ณผ CPU ์ˆ˜๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ์ƒ์œ„ ๊ฐ’์€ Agent + Peers ํ•ฉ๊ณ„๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋ฒ”์œ„์˜ ์ธก์ • ์œ ํšจ์„ฑ์„ ๋ณ„๋„๋กœ ํŒ๋‹จํ•˜๋ฏ€๋กœ Agent ์ธก์ • ์‹คํŒจ๊ฐ€ ์ธก์ •๋œ Peer ๊ฐ’์„ ๊ฐ€๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ •์ƒ ์กฐํšŒ ๊ฒฐ๊ณผ ๋นˆ Peer ์ธ๋ฒคํ† ๋ฆฌ๋Š” containers: 0, measuredContainers: 0, complete: true์™€ ์‚ฌ์šฉ๋Ÿ‰ 0์„ ๊ธฐ๋กํ•˜๋ฉฐ ๋ˆ„๋ฝยท์‹คํŒจ ํ‘œ๋ณธ์„ 0์œผ๋กœ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

GET /api/v1/agents/resources๋Š” {generatedAt, agents, totals, totalsByScope, services}๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. totals๋Š” ์›์‹œ cpuCores์™€ ๋ฉ”๋ชจ๋ฆฌ ํ•„๋“œ๋ฅผ ์œ ์ง€ํ•˜๊ณ  cpuPercent, cpuCapacityCores, cpuMeasuredAgents, measuredAgents, totalAgents, partialAgents, containers, measuredContainers๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. cpuPercent๋Š” ์ตœ์‹  ํ‘œ๋ณธ์„ ํ˜ธ์ŠคํŠธ CPU ์šฉ๋Ÿ‰์œผ๋กœ ๊ฐ€์ค‘ ๊ณ„์‚ฐํ•˜๋ฉฐ CPU ์ˆ˜ ๋ฏธ์ƒ Agent๋Š” ๋ถ„์žยท๋ถ„๋ชจ ๋ชจ๋‘ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ์›์‹œ ์ฝ”์–ดยท๋ฉ”๋ชจ๋ฆฌ ํ•ฉ๊ณ„์˜ Agent ๋ฒ”์œ„๋Š” ๋” ๋„“์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๊ฐœ์ˆ˜๋ฅผ ํ•จ๊ป˜ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค. ?format=csv๋Š” Agent๋ณ„ complete/partial/๋ฏธ์ƒ ์ƒํƒœ, cpu_percent_host, cpu_capacity_cores, ๋‘ ๋ฉ”๋ชจ๋ฆฌ ๊ฐ’, ์ปจํ…Œ์ด๋„ˆ ๊ฐœ์ˆ˜์™€ TOTAL ํ–‰์„ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋ˆ„๋ฝ๊ฐ’์€ 0์ด ์•„๋‹Œ ๋นˆ ์…€์ž…๋‹ˆ๋‹ค. totals๋Š” ๊ธฐ์กด ํ•ฉ๊ณ„์ด๋ฉฐ totalsByScope๋Š” agent_and_peers, agent, peers ๊ฐ๊ฐ์˜ ์ธก์ • ๋ฒ”์œ„์™€ CPU ์šฉ๋Ÿ‰ ๊ฐ€์ค‘ ๋ถ„๋ชจ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ CSV๋Š” ๋ถ„๋ฆฌ๊ฐ’์„ ๋ณด๊ณ ํ•œ Agent์˜ ๋ฒ”์œ„๋ณ„ ํ–‰๊ณผ ์œ ํšจ ์ธก์ •์ด ์žˆ๋Š” ๋ฒ”์œ„๋ณ„ TOTAL ํ–‰์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

GET /api/v1/agents/resources/history?range=1h๋Š” Prometheus ์›๋ณธ ํ‘œ๋ณธ์„ {from, to, scope, series}๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ์ƒ์œ„ scope๋Š” all์ด๋ฉฐ ๊ฐ ์‹œ๋ฆฌ์ฆˆ๋Š” agentId, scope(agent_and_peers, agent, peers), metric, unit, samples: [{at, value}], mean, min, max๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. format=csv&kind=samples ๋˜๋Š” format=csv&kind=summary๋กœ ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. CPU๋Š” cpu_percent ์ง€ํ‘œยทpercent_host ๋‹จ์œ„(ํ˜ธ์ŠคํŠธ ์ „์ฒด=100%), ๋ฉ”๋ชจ๋ฆฌ๋Š” bytes, ์‹œ๊ฐ์€ UTC์ž…๋‹ˆ๋‹ค. ์ปจํ…Œ์ด๋„ˆ ์ธก์ • ๋ฒ”์œ„๋„ containers_expected, containers_measured ์‹œ๋ฆฌ์ฆˆ์™€ containers ๋‹จ์œ„๋กœ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ํ•ฉ๊ณ„ CPU๋Š” kpl_agent_cpu_usage_percent, ๋ถ„๋ฆฌ CPU๋Š” kpl_agent_component_cpu_usage_percent gauge๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ์›์‹œ ์ฝ”์–ด gauge์™€ ์„ž์ง€ ์•Š์œผ๋ฉฐ ๊ฐ ์ด๋ ฅ์€ ํ•ด๋‹น gauge๊ฐ€ ์ฒ˜์Œ ์ˆ˜์ง‘๋œ ์‹œ์ ๋ถ€ํ„ฐ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. range๋Š” 1mโ€“24h์ž…๋‹ˆ๋‹ค. ๋ถ€๋ถ„ ์ธก์ •๊ฐ’๋„ ํ†ต๊ณ„์— ํฌํ•จํ•˜๋˜ ๋ˆ„๋ฝ ์‚ฌ์šฉ๋Ÿ‰์€ ์ถ”์ •ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ˆ„๋ฝ ๊ตฌ๊ฐ„๊ณผ ๋น„์œ ํ•œ ๊ฐ’์€ ์ œ์™ธํ•˜๊ณ  ๋™์ผ ์ค‘๋ณต์€ ํ•œ ๋ฒˆ๋งŒ ๊ณ„์‚ฐํ•˜๋ฉฐ ๊ฐ™์€ Agentยทscopeยท์ง€ํ‘œยท์‹œ๊ฐ์˜ ์ถฉ๋Œ๊ฐ’์€ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. ํ‰๊ท ์€ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ scrape ํ‘œ๋ณธ ๊ธฐ์ค€์ž…๋‹ˆ๋‹ค.

GET /api/v1/agents/resources/measurements๋Š” {generatedAt, maxDurationSeconds, measurements}๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ตœ์‹  ๊ตฌ๊ฐ„๋ถ€ํ„ฐ {id, startedAt, endedAt?, endReason?} ํ˜•์‹์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ๊ฒฝ๋กœ์˜ POST๋Š” ๊ณต์œ  ๊ตฌ๊ฐ„์„ ์‹œ์ž‘(201)ํ•˜๊ฑฐ๋‚˜ ์ด๋ฏธ ์ง„ํ–‰ ์ค‘์ธ ๊ตฌ๊ฐ„์„ ๋ฐ˜ํ™˜(200)ํ•˜๋ฉฐ ์ˆ˜์ง‘๊ธฐ๋ฅผ ์žฌ์‹œ์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. POST /api/v1/agents/resources/measurements/{id}/stop์€ ์ข…๋ฃŒ ์‹œ๊ฐ์„ ํ™•์ •ํ•ฉ๋‹ˆ๋‹ค(200). ๋ฐ˜๋ณต ์ข…๋ฃŒ๋Š” ๊ธฐ์กด ์ข…๋ฃŒ ์‹œ๊ฐ์„ ๋ณด์กดํ•˜๊ณ , ์ด์ „ ID์˜ ์ข…๋ฃŒ ์š”์ฒญ์€ ํ˜„์žฌ ๊ตฌ๊ฐ„์„ ๋ฉˆ์ถ”์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ API๋Š” ๋‹ค๋ฅธ Dashboard ์ž‘์—…๊ณผ ๊ฐ™์€ ๋ธŒ๋ผ์šฐ์ € ์„ธ์…˜ยทCSRF ๊ฒ€์‚ฌ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ธ๋ฑ์Šค๋Š” Controller ๋กœ์ปฌ ์ €์žฅ์†Œ์— ์›์ž์ ์œผ๋กœ ๊ต์ฒด ์ €์žฅํ•˜๊ณ  ์žฌ์‹œ์ž‘ ์‹œ ๋ณต์›ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ฝ๊ธฐยท์“ฐ๊ธฐ ์‹คํŒจ๋Š” ์ƒํƒœ๋ฅผ ๋ฒ„๋ฆฌ์ง€ ์•Š๊ณ  503์œผ๋กœ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.

์ด๋ ฅ๊ณผ ๊ตฌ๊ฐ„ ๋‚ด๋ณด๋‚ด๊ธฐ๋Š” ๊ธฐ์กด ํ•ฉ๊ณ„ gauge์™€ ์ƒˆ kpl_agent_component_* gauge๋ฅผ ์ œํ•œ๋œ ๋‹จ์ผ ์ฟผ๋ฆฌ๋กœ ํ•จ๊ป˜ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. CSV ์นผ๋Ÿผ์€ ์œ ์ง€ํ•˜๊ณ  ํ–‰๋งˆ๋‹ค scope๋กœ ๊ตฌ๋ถ„ํ•˜๋ฉฐ ํ†ต๊ณ„์™€ ์ค‘๋ณต ๊ฒ€์‚ฌ๋Š” scope๋ณ„๋กœ ๋…๋ฆฝ์ ์ž…๋‹ˆ๋‹ค. ๋ถ„๋ฆฌ ์‹œ๋ฆฌ์ฆˆ๋Š” ์—…๊ทธ๋ ˆ์ด๋“œ๋œ Agent๊ฐ€ ๋ณด๊ณ ํ•œ ์‹œ์ ๋ถ€ํ„ฐ ์ƒ์„ฑํ•˜๊ณ  ๊ธฐ์กด ํ•ฉ๊ณ„ ์ „์šฉ ์ด๋ ฅ์€ ๋ถ„๋ฆฌ๊ฐ’์„ ์ถ”์ •ํ•˜์ง€ ์•Š๊ณ  ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

DELETE /api/v1/agents/resources/measurements/{id}๋Š” ์™„๋ฃŒ ๊ตฌ๊ฐ„ ํ•˜๋‚˜๋ฅผ ์‚ญ์ œํ•˜๊ณ  ๊ฐฑ์‹ ํ•œ ์ธก์ • ๋ชฉ๋ก์„ 200์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ง„ํ–‰ ์ค‘ ๊ตฌ๊ฐ„์€ 409, ์—†๊ฑฐ๋‚˜ ์ด๋ฏธ ์‚ญ์ œ๋œ ID๋Š” 404์ž…๋‹ˆ๋‹ค. ์„ธ์…˜ยทCSRF ๋ณดํ˜ธ์™€ ๋กœ์ปฌ ์›์ž์  ์ €์žฅ์„ ์‚ฌ์šฉํ•˜๋ฉฐ ์“ฐ๊ธฐ ์‹คํŒจ ์‹œ ์‹ค์ œ ๊ธฐ๋ก์„ ์ง€์šฐ์ง€ ์•Š๊ณ  503์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ €์žฅ ๊ตฌ๊ฐ„์œผ๋กœ์˜ ๋‚ด๋ณด๋‚ด๊ธฐ๋Š” ๋” ์ด์ƒ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ๊ตฌ๊ฐ„์„ ํ™•๋ณดํ•œ ๋‹ค์šด๋กœ๋“œ๋Š” ์™„๋ฃŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณต์šฉ Prometheus ํ‘œ๋ณธ๊ณผ ์ด๋ฏธ ๋‚ด๋ ค๋ฐ›์€ CSV๋Š” ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

GET /api/v1/agents/resources/measurements/{id}/export๋Š” ์ €์žฅ๋œ ๊ตฌ๊ฐ„์— ๋Œ€ํ•ด ์œ„์™€ ๊ฐ™์€ ์ด๋ ฅ JSON์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. format=csv&kind=samples ๋˜๋Š” format=csv&kind=summary๋กœ ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. scrape ์‹œ๊ฐ์ด (startedAt, endedAt]์ธ ํ‘œ๋ณธ์„ ์„ ํƒํ•˜๋ฉฐ ์†Œ์ˆ˜ ์ดˆ ๊ฒฝ๊ณ„๋ฅผ ๋ณด์กดํ•˜๊ณ , ์žฌ๋‹ค์šด๋กœ๋“œ ์‹œ์—๋„ ์‹œ์ž‘ยท์ข…๋ฃŒ ์‹œ๊ฐ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ง„ํ–‰ ์ค‘ ๊ตฌ๊ฐ„์€ ์ข…๋ฃŒ ์ „๊นŒ์ง€ 409, ์•Œ ์ˆ˜ ์—†๋Š” ID ๋˜๋Š” CSV ํ‘œ๋ณธ ๋ถ€์žฌ๋Š” 404์ž…๋‹ˆ๋‹ค. ์งง์€ ๊ตฌ๊ฐ„๋„ ํ—ˆ์šฉํ•˜์ง€๋งŒ ํ‘œ๋ณธ์ด ์—†์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ตฌ๊ฐ„์€ 24์‹œ๊ฐ„ ํ›„ ์ž๋™ ์ข…๋ฃŒ(endReason: "time_limit", ์ˆ˜๋™ ์ข…๋ฃŒ๋Š” "stopped")ํ•˜๋ฉฐ ์ตœ๊ทผ 100๊ฐœ ๊ตฌ๊ฐ„์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ํ‘œ๋ณธ ๋ณด์กด ๊ธฐ๊ฐ„์€ Prometheus ์„ค์ •์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์ตœ๊ทผ ์ด๋ ฅ ์กฐํšŒ์™€ ๊ฐ™์€ ๋‚ด๋ณด๋‚ด๊ธฐ ์ œํ•œ์„ ์ ์šฉํ•˜๊ณ , ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ ํ›„์—๋„ ๊ตฌ๊ฐ„์„ ์œ ์ง€ํ•ด ์žฌ์‹œ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  ๋ฆฌ์†Œ์Šค endpoint๋Š” ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ฅ์€ Controller์— ์„ค์ •๋œ Prometheus ์ฃผ์†Œ์™€ ๊ณ ์ •๋œ ๋ฆฌ์†Œ์Šค selector๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ์š”์ฒญ์ž๊ฐ€ ์ž„์˜ backend URL์ด๋‚˜ PromQL์„ ์ง€์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. upstream ์กฐํšŒ ๋™์‹œ 2๊ฐœยท15์ดˆยท16 MiBยท250,000๊ฐœ ํ‘œ๋ณธ ์ œํ•œ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ parameter๋Š” 400, CSV ํ‘œ๋ณธ ๋ถ€์žฌ๋Š” 404, Prometheus ์‘๋‹ต ์‹คํŒจยทํฌ๊ธฐ ์ดˆ๊ณผยท๋ถˆ์™„์ „ ์‘๋‹ต์€ 502, ์กฐํšŒ ์Šฌ๋กฏ ๋ถ€์กฑ์€ 503์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ CSV๋Š” Prometheus ์—ฐ๊ฒฐ ์—†์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ์†Œ์Šค ์ด๋ ฅ์€ ์‹คํ—˜ ZIP๊ณผ ๋ณ„๋„์ด๋ฉฐ KPL ๋ฆฌ์†Œ์Šค ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

์„œ๋น„์Šค ์ž์› ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ

GET /api/v1/services/resources๋Š” Dashboard ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•˜๋ฉฐ {generatedAt, controllerUptimeSeconds, services}๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋น„์Šค ํ•ญ๋ชฉ์€ service, scope, nodeId, nodeName, reportedAt (Controller ์ˆ˜์‹  ์‹œ๊ฐ), state, ์„ ํƒ์  resources์™€ error๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ๋Š” running, not_running, unknown, stale, awaiting์ด๋ฉฐ ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ ์ƒํƒœ์™€ ์ž์› ํ‘œ๋ณธ ์œ ํšจ์„ฑ์€ ๋ณ„๊ฐœ์ž…๋‹ˆ๋‹ค. ์ž์› ํ•„๋“œ๋Š” ๊ธฐ์กด Agent ํ•ฉ๊ณ„์™€ ๊ฐ™์€ ์‹œ๊ฐยทํ˜ธ์ŠคํŠธ CPU ์ˆ˜ยท๋ฉ”๋ชจ๋ฆฌยท์ปค๋ฒ„๋ฆฌ์ง€ ํ˜•์‹์ด๋ฉฐ Agent/Peer ํ•˜์œ„ ๋ฒ”์œ„๋Š” ์—†์Šต๋‹ˆ๋‹ค. /api/v1/agents/resources๋„ services๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉฐ ๊ธฐ์กด Agent ํ•ฉ๊ณ„๋Š” ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

POST /api/v1/services/resources/report๋Š” ๋‚ด๋ถ€ Bearer ์ธ์ฆ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ณธ๋ฌธ์€ {nodeId, nodeName, reportedAt, services: [{service, state, resources?}], error?}์ด๋ฉฐ ๋ณด๊ณ  ์ƒํƒœ๋Š” running, not_running, unknown์ž…๋‹ˆ๋‹ค. ์š”์ฒญ 128 KiB, ๋…ธ๋“œ๋‹น ์ค‘๋ณต ์—†๋Š” ์„œ๋น„์Šค 64๊ฐœ, ์„œ๋น„์Šค๋‹น ์ปจํ…Œ์ด๋„ˆ 512๊ฐœ๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. ID๋Š” ๊ธธ์ด๊ฐ€ ์ œํ•œ๋œ ๋‹จ์ˆœ ์‹๋ณ„์ž์ž…๋‹ˆ๋‹ค. ํ‘œ๋ณธ ์‹œ๊ฐ์€ Controller ์‹œ๊ณ„๋กœ ์ •๊ทœํ™”ํ•˜๊ณ  ์ˆœ์„œ๊ฐ€ ๋’ค๋Šฆ์€ ๋ณด๊ณ ๋Š” ๋ฌด์‹œํ•ฉ๋‹ˆ๋‹ค. ์บ์‹œ๋Š” ๋…ธ๋“œ ์ตœ๋Œ€ 512๊ฐœ์ด๋ฉฐ 30์ดˆ ๋’ค stale, 10๋ถ„ ๋™์•ˆ ๋ฏธ์ˆ˜์‹ ํ•˜๋ฉด ๋งŒ๋ฃŒ๋ฉ๋‹ˆ๋‹ค. ์ •์ƒ ๋ณด๊ณ ๋Š” 204, ์ž˜๋ชป๋œ ๋ณด๊ณ  400, ์ธ์ฆ ์‹คํŒจ 401, ์บ์‹œ ํ•œ๋„ ์ดˆ๊ณผ 503์ž…๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ € ์„ธ์…˜์œผ๋กœ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ๋ฅผ ์ œ์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์ž์› ์ด๋ ฅ๊ณผ ์ €์žฅ ๊ตฌ๊ฐ„ ๋‚ด๋ณด๋‚ด๊ธฐ๋Š” ๊ฐ™์€ ์ œํ•œ๋œ Prometheus ์š”์ฒญ์— kpl_service_* ์ง€ํ‘œ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ๊ฐ series์—๋Š” entityType (agent ๋˜๋Š” service)์ด ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. ์„œ๋น„์Šค series์—๋Š” serviceยทnodeId๊ฐ€ ์žˆ์œผ๋ฉฐ agentId๋Š” ๋น„์šฐ๊ณ  scope=controller|third_party|monitor๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. running๊ณผ resource_report_fresh์˜ ๋‹จ์œ„๋Š” boolean์ด๊ณ  CPUยท๋ฉ”๋ชจ๋ฆฌยท์ปค๋ฒ„๋ฆฌ์ง€์˜ ์ด๋ฆ„๊ณผ ๋‹จ์œ„๋Š” ๊ธฐ์กด๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ํ†ต๊ณ„์™€ ์ค‘๋ณต ๊ฒ€์‚ฌ ํ‚ค๋Š” scope๋ฟ ์•„๋‹ˆ๋ผ ์„œ๋น„์Šค์™€ ํ˜ธ์ŠคํŠธ๋ฅผ ๋ชจ๋‘ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ฐ’ยท์ด๋ ฅยท์š”์•ฝ CSV์˜ ๊ธฐ์กด ์—ด ๋’ค์— entity_type,service,node_id,node_name์„ ์ถ”๊ฐ€ํ•˜๋ฉฐ ์ด๋ ฅ์˜ node_name์€ ๋น„์›๋‹ˆ๋‹ค. ๊ธฐ์กด Agent ํ•ฉ๊ณ„/ํ•˜์œ„ ๋ฒ”์œ„ ์ด๋ ฅ๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. Controller ๋ฐ ๊ด€๋ฆฌ ์„œ๋น„์Šค๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.

์‹œ๋‚˜๋ฆฌ์˜ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

POST /api/v1/scenarios/validate๋Š” ์›๋ณธ YAML ๋ณธ๋ฌธ(Content-Type: application/yaml)์„ ์ตœ๋Œ€ 1 MiB๊นŒ์ง€ ๋ฐ›์Šต๋‹ˆ๋‹ค. ์œ ํšจํ•˜๋ฉด 200๊ณผ {valid: true, name, phases}๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋นˆ ์ž…๋ ฅ, YAML ๋ฌธ๋ฒ• ์˜ค๋ฅ˜, ์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ, ์ž˜๋ชป๋œ ์„ค์ •, ์—ฌ๋Ÿฌ YAML ๋ฌธ์„œ๋Š” 400๊ณผ {error: "โ€ฆ"}๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ํŒŒ์„œ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ์ค„ ๋ฒˆํ˜ธ๋ฅผ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ๋ณธ๋ฌธ ํ•œ๋„ ์ดˆ๊ณผ๋Š” 413์ž…๋‹ˆ๋‹ค. ์ €์žฅยท์‹คํ–‰๊ณผ ๊ฐ™์€ ํŒŒ์„œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ ๊ธฐ๋ก์ด๋‚˜ job์„ ๋งŒ๋“ค์ง€ ์•Š์œผ๋ฉฐ ๋กœ๊ทธ์ธ ์„ธ์…˜์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์„ค์ • ๊ฒ€์ฆ์ด๋ฉฐ Agent ์šฉ๋Ÿ‰์ด๋‚˜ ์‹คํ–‰ ์‹œ ์—ฐ๊ฒฐ์„ฑ์„ ๊ฒ€์‚ฌํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.

์š”์ฒญ๊ณผ ์‘๋‹ต ๋ณธ๋ฌธ์€ ๋ชจ๋‘ JSON์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋„ ๊ฐ€๋ณ๊ฒŒ ์—ด ์ˆ˜ ์žˆ๋„๋ก ๋ชฉ๋ก ์‘๋‹ต์—๋Š” YAML์„ ๋„ฃ์ง€ ์•Š์œผ๋ฏ€๋กœ, ํŽธ์ง‘ํ•˜๊ธฐ ์ „์— ๊ฐœ๋ณ„ ํ•ญ๋ชฉ์„ ์กฐํšŒํ•˜์‹ญ์‹œ์˜ค.

Method Path ๋ณธ๋ฌธ ์„ฑ๊ณต ์‘๋‹ต
GET /api/v1/scenarios ์—†์Œ id, name, createdAt, updatedAt ์š”์•ฝ ๋ชฉ๋ก
POST /api/v1/scenarios { "name": "โ€ฆ", "yaml": "โ€ฆ" } ์ „์ฒด ํ•ญ๋ชฉ๊ณผ 201
GET /api/v1/scenarios/{id} ์—†์Œ yaml์„ ํฌํ•จํ•œ ์ „์ฒด ํ•ญ๋ชฉ
PUT /api/v1/scenarios/{id} { "name": "โ€ฆ", "yaml": "โ€ฆ" } ๊ฐฑ์‹ ๋œ ์ „์ฒด ํ•ญ๋ชฉ๊ณผ 200
DELETE /api/v1/scenarios/{id} ์—†์Œ ๋ณธ๋ฌธ ์—†๋Š” 204

์กฐํšŒยท๊ฒ€์ฆยท๋ณ€๊ฒฝ ๋ชจ๋‘ ๋กœ๊ทธ์ธ ์„ธ์…˜์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ ์š”์ฒญ์˜ X-KPL-Request: dashboard ํ—ค๋”๋Š” UI๊ฐ€ ์ž๋™์œผ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ํ•ด์„ํ•œ YAML์ด 1 MiB๋ฅผ ๋„˜๋Š” ๊ฒฝ์šฐ๋ฅผ ํฌํ•จํ•˜์—ฌ ์œ ํšจํ•˜์ง€ ์•Š์€ ์ž…๋ ฅ์€ 400์ž…๋‹ˆ๋‹ค. JSON ์š”์ฒญ ๋ณธ๋ฌธ์€ escape๋œ ๋ฌธ์ž๋ฅผ ๊ณ ๋ คํ•ด 6 * 1 MiB + 64 KiB๊นŒ์ง€ ํ—ˆ์šฉํ•˜๋ฉฐ ์ด ํ•œ๋„๋ฅผ ๋„˜์œผ๋ฉด 413์ž…๋‹ˆ๋‹ค. ์•Œ ์ˆ˜ ์—†๋Š” JSON ํ•„๋“œ์™€ ํ›„ํ–‰ JSON ๊ฐ’์€ ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์€ ID๋Š” 404, ์ €์žฅ์†Œ ์˜ค๋ฅ˜๋Š” 500์ž…๋‹ˆ๋‹ค. ID๋Š” ์†Œ๋ฌธ์ž 16์ง„์ˆ˜ 32์ž๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

control-node:8080์€ access๊ฐ€ ํ‘œ์‹œํ•œ Controller ์ฃผ์†Œ๋กœ ๋ฐ”๊พธ๊ณ , ๋จผ์ € API ์ธ์ฆ์œผ๋กœ KPL_COOKIE_JAR๋ฅผ ์ƒ์„ฑํ•˜์‹ญ์‹œ์˜ค.

curl --fail -b "${KPL_COOKIE_JAR:?Log in first}" http://control-node:8080/api/v1/scenarios

curl --fail -X POST http://control-node:8080/api/v1/scenarios \
  -H 'Content-Type: application/json' \
  -b "${KPL_COOKIE_JAR:?Log in first}" -H 'X-KPL-Request: dashboard' \
  --data-binary @- <<'JSON'
{"name":"Smoke baseline","yaml":"version: 2\nname: smoke-baseline\nphases:\n  - action: stop-all\n"}
JSON

๊ณตํ†ต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ทธ๋ฃน

์‹œ๋‚˜๋ฆฌ์˜ค์™€ ์ €์žฅ ๊ฒฐ๊ณผ๋Š” ์ •๋ฆฌ์šฉ ๊ทธ๋ฃน ๋ชฉ๋ก์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ํ•ญ๋ชฉ์€ ์ตœ๋Œ€ ํ•œ ๊ทธ๋ฃน์— ์†ํ•˜๋ฉฐ, ์†Œ์† ๊ธฐ๋ก์ด ์—†์œผ๋ฉด Ungrouped์ž…๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ •๋ฆฌ์šฉ ๊ทธ๋ฃน์€ ๋ฐ˜๋ณต ์‹คํ–‰ ๋ฐฐ์น˜, ๋ฐฐ์น˜ ๋ฉ”๋ชจ, ์‹œ๋‚˜๋ฆฌ์˜ค์˜ Peer ๊ทธ๋ฃน ๋ฐ ๋ถ„์„ ๋ฒ”์œ„์™€ ๋…๋ฆฝ์ ์ž…๋‹ˆ๋‹ค.

๋ชจ๋“  endpoint๋Š” ์„ฑ๊ณต ์‹œ ์ „์ฒด snapshot {revision, groups, memberships}๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. groups๋Š” {id, name} ๋ฐฐ์—ด์ด๋ฉฐ, memberships๋Š” ํ•ญ๋ชฉ ํ‚ค๋ฅผ ๊ทธ๋ฃน ID์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฃน ID๋Š” ์†Œ๋ฌธ์ž 16์ง„์ˆ˜ 32์ž์ž…๋‹ˆ๋‹ค. ์ตœ์ดˆ ๋นˆ snapshot์€ {"revision":"0","groups":[],"memberships":{}}์ž…๋‹ˆ๋‹ค. ์ดํ›„ revision์€ ํ•ด์„ํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ž์—ด ํ† ํฐ์œผ๋กœ ์ทจ๊ธ‰ํ•˜๊ณ  ๋ชจ๋“  ๋ณ€๊ฒฝ ์š”์ฒญ์— ์ตœ์‹  ๊ฐ’์„ ์ „๋‹ฌํ•˜์‹ญ์‹œ์˜ค.

Method Path JSON ๋ณธ๋ฌธ ์„ฑ๊ณต ์‘๋‹ต
GET /api/v1/library-groups ์—†์Œ ํ˜„์žฌ snapshot๊ณผ 200
POST /api/v1/library-groups {name, revision} ์ƒˆ ๊ทธ๋ฃน์ด ํฌํ•จ๋œ snapshot๊ณผ 201
PUT /api/v1/library-groups/members {groupId, keys, revision} ์„ ํƒ ํ•ญ๋ชฉ ์ „์ฒด ์ด๋™ ํ›„ 200
PATCH /api/v1/library-groups/{id} {name, revision} ๊ทธ๋ฃน ์ด๋ฆ„ ๋ณ€๊ฒฝ ํ›„ 200
DELETE /api/v1/library-groups/{id} {revision} ๊ทธ๋ฃน๊ณผ ํ•ด๋‹น ์†Œ์† ๊ธฐ๋ก ์ œ๊ฑฐ ํ›„ 200

ํ•ญ๋ชฉ ํ‚ค๋Š” ์ €์žฅ ์‹œ๋‚˜๋ฆฌ์˜ค์˜ scenario:<id>, ๋ฐ˜๋ณต ์‹คํ–‰ ๋ฐฐ์น˜ ์ „์ฒด์˜ batch:<batchId>, ๋ฐฐ์น˜ ์—†๋Š” ๊ฒฐ๊ณผ์˜ run:<id>์ž…๋‹ˆ๋‹ค. ๋ฐฐ์น˜๋Š” ์ดํ›„ ์ถ”๊ฐ€ยท์žฌ์‹œ๋„ํ•œ Run์„ ํฌํ•จํ•˜์—ฌ ์†Œ์† ๊ธฐ๋ก ํ•˜๋‚˜๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ์†Œ์†์„ ํ•ด์ œํ•˜๋ ค๋ฉด groupId: ""๋กœ ์ด๋™ํ•˜์‹ญ์‹œ์˜ค. ํ•ญ๋ชฉ ์ด๋™๊ณผ ๊ทธ๋ฃน ์‚ญ์ œ๋Š” ํ•˜๋‚˜์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐฑ์‹ ์œผ๋กœ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฃน์„ ์‚ญ์ œํ•ด๋„ ์‹œ๋‚˜๋ฆฌ์˜ค YAML, Run, ๋ฐฐ์น˜ ์‹๋ณ„์ž, ๋ฉ”๋ชจ, ์•„์นด์ด๋ธŒ์™€ ๋ถ„์„์€ ๋ณด์กด๋˜๋ฉฐ ํ•ด๋‹น ํ•ญ๋ชฉ์€ Ungrouped๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

๋ชจ๋“  endpoint์— ๋กœ๊ทธ์ธ ์„ธ์…˜์ด ํ•„์š”ํ•˜๋ฉฐ ๋ณ€๊ฒฝ ์š”์ฒญ์—๋Š” X-KPL-Request: dashboard๋„ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์‘๋‹ต์€ Cache-Control: no-store๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ž˜๋œ revision ๋˜๋Š” ์ค‘๋ณต ์ด๋ฆ„์€ 409๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ snapshot์„ ๋‹ค์‹œ ์กฐํšŒํ•˜๊ณ  ๋ณ€๊ฒฝ ๋‚ด์šฉ์„ ํ™•์ธํ•œ ๋’ค ๋ช…์‹œ์ ์œผ๋กœ ์žฌ์‹œ๋„ํ•˜์‹ญ์‹œ์˜ค. ์—†๋Š” ๊ทธ๋ฃน ๋˜๋Š” ๊ทธ๋ฃน์— ๋„ฃ์œผ๋ ค๋Š” ํ•ญ๋ชฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์œผ๋ฉด 404์ž…๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ํ•„๋“œยทํ‚คยท์ด๋ฆ„๊ณผ ๊ทธ๋ฃนยท์†Œ์† ๊ฐœ์ˆ˜ ํ•œ๋„ ์ดˆ๊ณผ๋Š” 400์ด๋ฉฐ, ์•Œ ์ˆ˜ ์—†๋Š” JSON ํ•„๋“œ์™€ ํ›„ํ–‰ ๊ฐ’์€ ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญ ๋ณธ๋ฌธ 2 MiB ์ดˆ๊ณผ๋Š” 413, ์ €์žฅ์†Œ ์˜ค๋ฅ˜๋Š” 500์ž…๋‹ˆ๋‹ค. ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์ผ๋ถ€ ํ•ญ๋ชฉ๋งŒ ์ด๋™ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ๋„คํŠธ์›Œํฌ ์‘๋‹ต์ด ๋ถˆํ™•์‹คํ•˜๋ฉด ์žฌ์‹œ๋„ ์ „์— ๋‹ค์‹œ ์กฐํšŒํ•˜์‹ญ์‹œ์˜ค.

์ด๋ฆ„์€ ์•ž๋’ค ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•œ ์œ ํšจ UTF-8 ๋ฌธ์ž 1โ€“128์ž๋กœ, ์ œ์–ด ๋ฌธ์ž๋ฅผ ํ—ˆ์šฉํ•˜์ง€ ์•Š์œผ๋ฉฐ Unicode ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š๋Š” ๋น„๊ต์—์„œ ๊ณ ์œ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ตœ๋Œ€ ๊ทธ๋ฃน 1,000๊ฐœ, ์†Œ์† ๊ธฐ๋ก 20,000๊ฐœ๋ฅผ ์ง€์›ํ•˜๊ณ  ํ•œ ๋ฒˆ์˜ ์ด๋™ ์š”์ฒญ์€ ํ‚ค 1โ€“2,000๊ฐœ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ์ตœ๋Œ€ 4 MiB์ธ ๋กœ์ปฌ <data-dir>/library-groups.json์— ์ €์žฅ๋˜์–ด Controller ์žฌ์‹œ์ž‘ ํ›„์—๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฃน ๋ณ€๊ฒฝ์€ ์‹คํ—˜ ์ž…๋ ฅ์ด๋‚˜ ๋ถ„์„ ํ•ด์‹œ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ฝ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํŒŒ์ผ์€ ๋นˆ ๊ฐ’์œผ๋กœ ๋Œ€์ฒดํ•˜์ง€ ์•Š๊ณ  ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. UI ์‚ฌ์šฉ ์ˆœ์„œ๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ๊ฒฐ๊ณผ๋ฅผ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

์‹คํ–‰ ์ œ์ถœ, ์ค‘์ง€์™€ ๊ด€์ธก

์‹ ๊ทœ ์ œ์ถœยท์žฌ์‹œ๋„ยท์ด์–ดํ•˜๊ธฐยท์ถ”๊ฐ€ ์‹คํ–‰์€ Controller ์ „์ฒด์˜ ์ ‘์ˆ˜ ์ˆœ์„œ ๋Œ€๊ธฐ์—ด์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. 202๋Š” ์ ‘์ˆ˜ ์™„๋ฃŒ์ด๋ฉฐ ์ฆ‰์‹œ ์‹คํ–‰์„ ๋œปํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ฐจ๋ก€๊ฐ€ ์˜ค๊ธฐ ์ „์—๋Š” ์ฒซ Run๋„ state: "queued", 0์ธ startedAt์„ ์œ ์ง€ํ•˜๋ฉฐ ์—ฌ๋Ÿฌ ์ œ์ถœ์„ ์˜ˆ์•ฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ experiment์˜ ์„ ํƒ ํ•„๋“œ queuePosition์€ ์•ž์„  ์ œ์ถœ ์ˆ˜(ํšŒ์ฐจ ์ˆ˜ ์•„๋‹˜)์ด๊ณ  0์ด๋ฉด ์ƒ๋žตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ์ œ์ถœ์˜ ๋ฐ˜๋ณต์€ ๋™์ผํ•œ ์œ„์น˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ๊ฒฐ๊ณผ manifest์—๋Š” ์ €์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹คํŒจยท์ทจ์†Œ๋Š” ํ•ด๋‹น ์ œ์ถœ์˜ ๋‚จ์€ ๋ฐ˜๋ณต๋งŒ ๋๋‚ด๊ณ , ์ด์ „ Peer ์ •๋ฆฌ ํ™•์ธ ํ›„ ๋‹ค์Œ ์ œ์ถœ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์ •๋ฆฌ๊ฐ€ ์‹คํŒจํ•˜๋ฉด ๋‹ค์Œ ์ œ์ถœ์˜ phaseName์— ์›์ธ์„ ํ‘œ์‹œํ•˜๋ฉฐ ์ž๋™ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ๋Œ€๊ธฐ ํ•ญ๋ชฉ์„ ์ค‘์ง€ํ•ด๋„ ์ดํ›„ ์ œ์ถœ์˜ ์ •๋ฆฌ ์กฐ๊ฑด์€ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ๋‹จ์ผ Run์ด ์œ ์ง€ํ•œ Peer๋„ ๋‹ค์Œ ์ œ์ถœ ์ „์— ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์•ฝ ๋Œ€๊ธฐ๋Š” phase๋‚˜ schedule ์‹œ๊ฐ„์„ ์†Œ๋ชจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Controller ์žฌ์‹œ์ž‘ ํ›„ ๋‚จ์€ ์˜ˆ์•ฝ์€ interrupted๋กœ ํ‘œ์‹œํ•˜๋ฉฐ ์ˆ˜๋™ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค. ์‹คํ—˜ ์˜ˆ์•ฝ์„ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

POST /api/v1/experiments๋Š” ์ฒซ run์˜ experiment ๊ฐ์ฒด์™€ 202๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. {scenario, repetitions, scenarioId?}๋ฅผ ์ œ์ถœํ•˜๋ ค๋ฉด Content-Type: application/json์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. scenario๋Š” YAML ๋ฌธ์ž์—ด์ด๋ฉฐ repetitions๋ฅผ ์ƒ๋žตํ•˜๋ฉด ๊ธฐ๋ณธ๊ฐ’์€ 1์ž…๋‹ˆ๋‹ค. ์„ ํƒ์ ์ธ scenarioId๋Š” ๊ทธ๋ฃน์„ ์ƒ์†ํ•  ์ €์žฅ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ content type์€ ์›์‹œ YAML๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์›์‹œ YAML๊ณผ JSON์—์„œ ํ•ด์„ํ•œ scenario ๋ฌธ์ž์—ด์˜ ํ•œ๋„๋Š” ๊ฐ๊ฐ 1 MiB์ž…๋‹ˆ๋‹ค. JSON ์š”์ฒญ ๋ณธ๋ฌธ์€ escape๋ฅผ ๊ณ ๋ คํ•ด 6 * 1 MiB + 64 KiB๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญ ๋ณธ๋ฌธ ์ž์ฒด๊ฐ€ ํ•œ๋„๋ฅผ ๋„˜์œผ๋ฉด 413, ํ•ด์„ํ•œ YAML ๋ฌธ์ž์—ด์ด ํ•œ๋„๋ฅผ ๋„˜๊ฑฐ๋‚˜ scenario/repetition์ด ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด 400์ž…๋‹ˆ๋‹ค.

scenarioId๋Š” ์†Œ๋ฌธ์ž 16์ง„์ˆ˜ 32์ž๋กœ ๋œ ์ €์žฅ ์‹œ๋‚˜๋ฆฌ์˜ค ID์ž…๋‹ˆ๋‹ค. Controller๋Š” ์ œ์ถœํ•œ scenario ๋ณธ๋ฌธ์„ ์‹คํ–‰ํ•˜๋ฏ€๋กœ ํŽธ์ง‘๊ธฐ์˜ ๋ฏธ์ €์žฅ ๋ณ€๊ฒฝ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹ ๊ทœ ์ ‘์ˆ˜ ์‹œ scenario:<scenarioId>์˜ ํ˜„์žฌ ์†Œ์†์„ ์ฝ๊ณ  batch:<newBatchId>์— ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค. repetitions๊ฐ€ 1์ด์–ด๋„ ๋ฐฐ์น˜ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์‹œ๋‚˜๋ฆฌ์˜ค์— ๊ทธ๋ฃน์ด ์—†๊ฑฐ๋‚˜ ID๋ฅผ ์ƒ๋žตํ•˜๊ฑฐ๋‚˜ ์›์‹œ YAML๋กœ ์ œ์ถœํ•˜๋ฉด ๊ฒฐ๊ณผ๋Š” Ungrouped๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์ดํ›„ ์›๋ณธ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ด๋™ยท์‚ญ์ œํ•ด๋„ ๋ณต์‚ฌํ•œ ์†Œ์†์„ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ยท์ด์–ดํ•˜๊ธฐยท์ถ”๊ฐ€ ์‹คํ–‰์€ ๊ธฐ์กด ๊ฒฐ๊ณผ ์†Œ์†์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ID๋Š” 400, ์‹ ๊ทœ ์ ‘์ˆ˜ ์‹œ ์ €์žฅ ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์—†์œผ๋ฉด 404์ž…๋‹ˆ๋‹ค. ์ €์žฅ ์‹œ๋‚˜๋ฆฌ์˜ค ์ฝ๊ธฐ ์˜ค๋ฅ˜์™€ ๊ทธ๋ฃน ์กฐํšŒยท์ €์žฅ ์‹คํŒจ๋Š” 500, ์†Œ์† ๊ฐœ์ˆ˜ ํ•œ๋„ ์ดˆ๊ณผ๋Š” 400์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ๊ทธ๋ฃน ์†Œ์†์€ ์‹คํ–‰ ์ ‘์ˆ˜ ํ™•์ • ์ „์— ์ €์žฅํ•˜๋ฏ€๋กœ ์ด ์˜ค๋ฅ˜๋“ค์€ ์‹คํ—˜ ์‹œ์ž‘ ์ „์— ์ ‘์ˆ˜๋ฅผ ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค.

Idempotency-Key๋Š” ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด ์„ ํƒ ์‚ฌํ•ญ์ด์ง€๋งŒ ์ž๋™ํ™”์—์„œ๋Š” ๊ณ ์œ  ํ‚ค(์ตœ๋Œ€ 128๋ฐ”์ดํŠธ)๋ฅผ ์ง€์ •ํ•˜๊ณ  ์‘๋‹ต์ด ๋ถˆํ™•์‹คํ•˜๋ฉด ๊ฐ™์€ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณธ๋ฌธยท๋ฐ˜๋ณต ์ˆ˜ยทscenarioId๋กœ ์žฌ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. UI๋Š” session storage๋กœ ์ž๋™ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ค‘๋ณต ์š”์ฒญ์€ Controller ์žฌ์‹œ์ž‘ ๋˜๋Š” ์›๋ณธ ์‹œ๋‚˜๋ฆฌ์˜ค ์‚ญ์ œ ํ›„์—๋„ ๊ธฐ์กด ์ ‘์ˆ˜ Run์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์žฌ์‹คํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฃน ์ƒ์†์„ ๋‹ค์‹œ ์ˆ˜ํ–‰ํ•˜๊ฑฐ๋‚˜ ์ดํ›„ ์ˆ˜๋™์œผ๋กœ ์ด๋™ํ•œ ๊ฒฐ๊ณผ ์†Œ์†์„ ๋ฎ์–ด์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฐ™์€ ํ‚ค์— ๋‹ค๋ฅธ ์ž…๋ ฅ(scenarioId ๋ณ€๊ฒฝ ํฌํ•จ)์€ 409, ์›๋ž˜ ๊ฒฐ๊ณผ๊ฐ€ ์‚ญ์ œ๋œ ํ‚ค๋Š” 410์ž…๋‹ˆ๋‹ค. ํ‚ค๋ฅผ ์ƒ๋žตํ•˜๋ฉด ์„ฑ๊ณตํ•œ ์š”์ฒญ๋งˆ๋‹ค ์ƒˆ ๋ฐฐ์น˜๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. run-submissions/๋ฅผ ๋ฐฑ์—…ํ•˜์‹ญ์‹œ์˜ค.

์ตœ์ดˆ ์ œ์ถœยท์žฌ์‹œ๋„ยท์ด์–ดํ•˜๊ธฐยท์ถ”๊ฐ€ ์‹คํ–‰๋งˆ๋‹ค ์ƒˆ executionId๋ฅผ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ Run ๊ฐ์ฒด์˜ ๊ฐ’์„ X-KPL-Execution: <executionId>์— ๋„ฃ์–ด POST /api/v1/experiments/{id}/stop์„ ํ˜ธ์ถœํ•˜์‹ญ์‹œ์˜ค. ๋กœ๊ทธ์ธ๊ณผ X-KPL-Request: dashboard๋„ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์‹คํ–‰ ID ๋ˆ„๋ฝยท๋ถˆ์ผ์น˜์™€ ๊ณผ๊ฑฐ ๊ตฌ์„ฑ์› ์ค‘์ง€๋Š” 409์ž…๋‹ˆ๋‹ค. ๊ฐ™์€ ์‹คํ–‰์— ๋Œ€ํ•ด ์ ‘์ˆ˜๋œ ์ค‘์ง€๋ฅผ ๋ฐ˜๋ณตํ•ด๋„ ์•ˆ์ „ํ•˜๋ฉฐ SSE์˜ stopRequested๋กœ ์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ ์ทจ์†Œ ์ง„ํ–‰์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

3ํšŒ ๋ฐ˜๋ณต JSON ์š”์ฒญ ๋ณธ๋ฌธ ์˜ˆ์ œ:

{"scenario":"version: 1\nname: repeat-example\nphases:\n  - action: wait\n    duration: 1s\n","repetitions":3}

๋ฐ˜๋ณต ์ œ์ถœ์€ ๋งค iteration์— ๋ณ„๋„ run ID์™€ ๊ฒฐ๊ณผ ๊ธฐ๋ก์„ ์˜ˆ์•ฝํ•˜๊ณ  batchId, iteration, repetitions๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ์ˆœ์ฐจ ์‹คํ–‰ํ•˜๋ฉฐ ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์ทจ์†Œ๋œ iteration์€ ๋Œ€๊ธฐ ์ค‘์ธ ๋‚˜๋จธ์ง€ ์‹คํ–‰๋„ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์‹คํ–‰ ๋ฌถ์Œ์ด ์ง„ํ–‰ ์ค‘์ผ ๋•Œ ํ•ด๋‹น ๋ฌถ์Œ์˜ ๊ตฌ์„ฑ์›์„ ์ค‘์ง€ํ•˜๋ฉด ๊ทธ ์‹คํ–‰ ๋ฌถ์Œ์„ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค. repetitions > 1์—์„œ๋Š” ๋งˆ์ง€๋ง‰์„ ํฌํ•จํ•œ ๋งค iteration์ด ์ตœ์ข… ์ƒํƒœ๋ฅผ ๊ธฐ๋กํ•˜๊ธฐ ์ „์— Peer๋ฅผ fenceํ•˜๊ณ  ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์„ฑ๊ณตํ•œ ๋‹จ์ผ ์‹คํ–‰์€ ์‹œ๋‚˜๋ฆฌ์˜ค ์ข…๋ฃŒ ์‹œ ์‹คํ–‰ ์ค‘์ธ Peer๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

POST /api/v1/result-batches/{batchId}/retry๋Š” ์ €์žฅ๋œ ๋ฏธ์™„๋ฃŒ ํšŒ์ฐจ๋ฅผ ํšŒ์ฐจ ์ˆœ์„œ๋Œ€๋กœ ์ˆ˜๋™ ์žฌ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด 10ํšŒ ์ค‘ 4ํšŒ์ฐจ๊ฐ€ ์‹คํŒจํ–ˆ๋‹ค๋ฉด ์™„๋ฃŒ๋œ 13ํšŒ์ฐจ๋Š” ์œ ์ง€ํ•˜๊ณ  410ํšŒ์ฐจ๋ฅผ ๊ฐ ํšŒ์ฐจ์˜ ์ฒซ phase๋ถ€ํ„ฐ ๋‹ค์‹œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋’ค์ชฝ ํšŒ์ฐจ ์ค‘ ์ด๋ฏธ ์™„๋ฃŒ๋œ ํšŒ์ฐจ๋„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์‹คํŒจยท์ค‘๋‹จ(์žฌ์‹œ์ž‘ ํ›„ ๋ฏธ์‹œ์ž‘์œผ๋กœ ๋‚จ์€ ๊ธฐ๋ก ํฌํ•จ)ยท์ทจ์†Œ ๊ธฐ๋ก์„ ๋Œ€์ƒ์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ์‹œ๋‚˜๋ฆฌ์˜ค์™€ seed๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ์ƒˆ run ID๋ฅผ ๋ถ€์—ฌํ•˜๋ฉฐ experiment/result ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์˜ previousRunIds์— ์ด์ „ ์‹œ๋„ ID๋ฅผ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ๋กœ๊ทธ๋Š” Previous attempts์—์„œ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ๊ณ , ํšŒ์ฐจ๋ณ„ ๊ฐ€์žฅ ์ตœ๊ทผ์˜ ์ €์žฅ๋œ ์‹œ๋„๋งŒ ๋ฐฐ์น˜ ์ง„ํ–‰๋ฅ ๊ณผ ํ‰๊ท ์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด /resume์€ ์‹œ๋„ํ•œ ํšŒ์ฐจ๋ฅผ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋ฏธ์‹œ์ž‘ ์ž”์—ฌ ํšŒ์ฐจ๋งŒ ์žฌ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

result-groups/{batchId}/batch.json์€ ๋ชจ๋“  ์ ‘์ˆ˜ ๋ชจ๋“œ์˜ ํ™•์ • ๊ฒฝ๊ณ„์ด๋ฉฐ ID๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋Š” ์ด์–ดํ•˜๊ธฐ์˜ ์ด์ „ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ํ™•์ • ์ „ ์ค‘๋‹จ์€ ๋ถ€๋ถ„ ์˜ˆ์•ฝ์„ ๋กค๋ฐฑํ•˜๊ณ  ํ™•์ •๋œ ๋ฏธ์™„๋ฃŒ ์ž‘์—…์€ interrupted๋กœ ๋‚จ๊น๋‹ˆ๋‹ค. superseded๋กœ ์ตœ์‹  ๊ฒฐ๊ณผ๋ฅผ ์‚ญ์ œํ•ด๋„ ์ด์ „ ์‹œ๋„๋ฅผ ์ง„ํ–‰๋ฅ ์— ๋‹ค์‹œ ํฌํ•จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์†์ƒ๋œ ๊ทธ๋ฃน ๊ธฐ๋ก์€ ํ•ด๋‹น ๊ทธ๋ฃน์„ ์ฐจ๋‹จํ•˜๊ณ  ์˜ค๋ฅ˜๋ฅผ ๊ธฐ๋กํ•˜๋˜ ๋‹ค๋ฅธ ๊ทธ๋ฃน์˜ Controller ์‹œ์ž‘์€ ๊ณ„์†ํ•ฉ๋‹ˆ๋‹ค.

๋‘ ์ด์–ดํ•˜๊ธฐ ๋ชจ๋“œ๋Š” ์ ‘์ˆ˜ํ•œ HTTP ์š”์ฒญ๊ณผ ๋…๋ฆฝ์ ์œผ๋กœ Controller์—์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ƒˆ ํšŒ์ฐจ ์‹คํ–‰ ์ „์— ์ด์ „ Peer๋ฅผ fenceยท์ œ๊ฑฐํ•˜๊ณ  Agent ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ •๋ฆฌ๊ฐ€ ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์ •๋ฆฌ ์ค‘ ์‚ฌ์šฉ์ž๊ฐ€ ์ •์ง€ํ•˜๋ฉด ๋Œ€๊ธฐ ํšŒ์ฐจ๋ฅผ canceled๋กœ ๊ธฐ๋กํ•˜๊ณ  experiment ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ด์œ ๋ฅผ ๋‚จ๊ธฐ๋ฉฐ ์ƒˆ ํšŒ์ฐจ๋Š” ์‹œ์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Agentยท์ •๋ฆฌ ์˜ค๋ฅ˜๋ฅผ ํ•ด๊ฒฐํ•œ ๋’ค ๋‹ค์‹œ ์ด์–ดํ•˜๊ธฐ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‹คํ–‰ยท์ตœ์ข… ์ •๋ฆฌยท๋‹ค์šด๋กœ๋“œยท๊ฐœ๋ณ„ ๋ฐ ๋ฐฐ์น˜ ๋ถ„์„์ด ์ง„ํ–‰ ์ค‘์ด๋ฉด ์ ‘์ˆ˜๋ฅผ ๊ฑฐ์ ˆํ•˜๋ฉฐ, ์™„๋ฃŒ๋œ ํšŒ์ฐจ ๊ธฐ๋ก์€ ๋ฎ์–ด์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

cleanupState(pending, running, complete, failed, retained)์™€ dataState(pending, collecting, complete, incomplete, unverified)๋Š” ์‹คํ–‰ state์™€ ๋ณ„๊ฐœ์ด๋ฉฐ cleanupError, integrityError์— ๋ฌธ์ œ๋ฅผ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ์ƒˆ Agent๋Š” runDrain์„ ์•Œ๋ฆฌ๊ณ  ๊ณผ๊ฑฐ Agent๋ฅผ ์‚ฌ์šฉํ•œ Run์€ unverified์ž…๋‹ˆ๋‹ค. ์ƒ์„ธ ๊ฒฐ๊ณผ์˜ agents, controllerVersion์€ ์‹คํ–‰ ํ™˜๊ฒฝ์ด๋ฉฐ SSE์—์„œ๋Š” ํฐ ํ™˜๊ฒฝ ๋ฐฐ์—ด์„ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. run-ownership/๋Š” create RPC ์ „์— ์ฐธ์—ฌ Agent๋ฅผ ๊ธฐ๋กํ•˜์—ฌ Agent ์žฌ๋“ฑ๋ก ์ „์—๋„ ์ •๋ฆฌ ๋Œ€์ƒ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

GET /api/v1/experiments, Dashboard snapshot/SSE์™€ ์ตœ์ดˆ ์ œ์ถœ ์‘๋‹ต์˜ ์‹ค์‹œ๊ฐ„ experiment ๊ฐ์ฒด์—๋Š” timing์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•„๋“œ๋Š” estimatedFinishAt(UTC), ์Œ์ˆ˜๊ฐ€ ์•„๋‹Œ remainingSeconds, basis(scenario ๋˜๋Š” observed-runs), observedRuns, overdue์ž…๋‹ˆ๋‹ค. ์ง„ํ–‰ ์ค‘์ธ ๋ฐ˜๋ณต Run์—๋Š” batchEstimatedFinishAt, batchRemainingSeconds, batchOverdue๋„ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค(0ยทfalse์ธ ์„ ํƒ ํ•„๋“œ๋Š” ์ƒ๋žต ๊ฐ€๋Šฅ). ๋Œ€๊ธฐ Run์˜ ์ข…๋ฃŒ ์‹œ๊ฐ์—๋Š” ๊ฐ™์€ ๋ฐฐ์น˜์˜ ์„ ํ–‰ Run ์‹œ๊ฐ„์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์™„๋ฃŒยท์‹คํŒจยท์ทจ์†Œ๋œ Run์ด๋‚˜ ์‹œ๊ฐ„ ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•œ ๊ฒฝ์šฐ timing์„ ์ƒ๋žตํ•˜๋ฉฐ ๊ฒฐ๊ณผ manifest์— ์ €์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ณ„์‚ฐ ๋ฐฉ์‹๊ณผ ๋ถˆํ™•์‹ค์„ฑ์€ ์˜ˆ์ƒ ์ข…๋ฃŒ ์‹œ๊ฐ์„ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

์ค‘์ง€ endpoint๋Š” cleanup ์™„๋ฃŒ ์ „, ์ทจ์†Œ ์š”์ฒญ์„ ์ ‘์ˆ˜ํ•˜๋ฉด 202์™€ JSON {"runId":"run-id","status":"stopping"}์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Dashboard๋Š” snapshot์—์„œ ์‹คํ–‰ยท๋Œ€๊ธฐ ์ค‘์ธ ๊ตฌ์„ฑ์›์ด ์—†์Œ์„ ํ™•์ธํ•  ๋•Œ๊นŒ์ง€ ํ•ด๋‹น ์‹คํ–‰ ๋˜๋Š” ๋ฐฐ์น˜์˜ ๋ฒ„ํŠผ์„ **Stoppingโ€ฆ**์œผ๋กœ ๋น„ํ™œ์„ฑํ™”ํ•˜๋ฉฐ, ์ ‘์ˆ˜ ์‘๋‹ต๋งŒ์œผ๋กœ ๋ฒ„ํŠผ์„ ๋‹ค์‹œ ํ™œ์„ฑํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. /api/v1/experiments ๋˜๋Š” snapshot์—์„œ ์ตœ์ข… ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค. ์ทจ์†Œ handle์ด ๋” ์ด์ƒ ์—†๋Š” run์€ 404์ž…๋‹ˆ๋‹ค. ๊ฒฝ๋Ÿ‰ยท๊ธฐ์กด ์ŠคํŠธ๋ฆผ ๊ณ„์•ฝ์€ Dashboard SSE๋ฅผ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค. /api/v1/events๋Š” ํ˜„์žฌ Controller ์ƒํƒœ์—์„œ ๊ฐ€์žฅ ์ตœ๊ทผ event ์ตœ๋Œ€ 300๊ฐœ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

์ €์žฅ์†Œ ๋ฃจํŠธ์—์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ํ˜ธ์ŠคํŠธ๋Š” access๊ฐ€ ํ‘œ์‹œํ•œ Controller ์ฃผ์†Œ๋กœ ๋ฐ”๊พธ๊ณ , ๋จผ์ € ์ธ์ฆ ์ ˆ์ฐจ๋กœ KPL_COOKIE_JAR๋ฅผ ์ƒ์„ฑํ•˜์‹ญ์‹œ์˜ค.

curl -X POST http://control-node:8080/api/v1/experiments \
  -H 'Content-Type: application/yaml' \
  -b "${KPL_COOKIE_JAR:?Log in first}" -H 'X-KPL-Request: dashboard' \
  --data-binary @examples/smoke.yaml

์‹œ๋‚˜๋ฆฌ์˜ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ endpoint๋Š” ์žฌ์‚ฌ์šฉํ•  ํŽธ์ง‘๊ธฐ ์ž…๋ ฅ์„ ์‹คํ—˜ ๊ฒฐ๊ณผ์™€ ๋ณ„๋„๋กœ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ๋ชฉ๋ก์—์„œ๋Š” YAML์„ ์ œ์™ธํ•˜๊ณ , ๊ฐœ๋ณ„ GETยทPOSTยทPUT ์‘๋‹ต์—๋Š” ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. UI ์‚ฌ์šฉ ์ˆœ์„œ์™€ ์ €์žฅ ์œ„์น˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์•ˆ๋‚ด, payload์™€ ๊ฒ€์ฆ ์ œํ•œ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ API๋ฅผ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

์™„๋ฃŒ ๊ทธ๋ฃน ํ™•์žฅ

POST /api/v1/result-batches/{batchId}/append๋Š” ๋กœ๊ทธ์ธ ์„ธ์…˜, X-KPL-Request: dashboard, JSON {"additionalRuns":10,"expectedRepetitions":10}์„ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค. ๋‘ ํ•„๋“œ๋Š” ์–‘์˜ ์ •์ˆ˜์ด๋ฉฐ ์ถ”๊ฐ€ ํ›„ ์ดํ•ฉ์€ 100 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๊ณตํ•˜๋ฉด ์ฒซ ์ƒˆ ๋Œ€๊ธฐ Run๊ณผ 202๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์‹œ์—์„œ๋Š” iteration: 11, repetitions: 20์ด๋ฉฐ ๊ธฐ์กด ๋ฐฐ์น˜ ID์™€ ์™„๋ฃŒ ๊ฒฐ๊ณผ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์‹คํ–‰์€ Controller ์ˆ˜๋ช…์— ์—ฐ๊ฒฐํ•˜์—ฌ ์š”์ฒญยท๋ธŒ๋ผ์šฐ์ € ์ข…๋ฃŒ๋กœ ์ทจ์†Œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ ‘์ˆ˜ ์ž ๊ธˆ ์•„๋ž˜ ํ˜„์žฌ ๊ทธ๋ฃน ์ดํ•ฉ์ด expectedRepetitions์™€ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋™์‹œยท์˜ค๋ž˜๋œ ์ค‘๋ณต ์š”์ฒญ์€ ๋‹ค์‹œ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๊ณ  409๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋ˆ„๋ฝยท๋ฏธ์™„๋ฃŒยท๋ถˆ์ผ์น˜ ํšŒ์ฐจ, ์„œ๋กœ ๋‹ค๋ฅธ ์ €์žฅ ์‹œ๋‚˜๋ฆฌ์˜ค, ํ™œ์„ฑ ๋ฐฐ์น˜ยท๋‹ค์šด๋กœ๋“œยท๋ถ„์„๋„ 409์ž…๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ๋ณธ๋ฌธยท์ˆ˜๋Ÿ‰์€ 400, ์—†๋Š” ๊ทธ๋ฃน์€ 404, ๋กœ์ปฌ ๊ณต๊ฐ„ ๋ถ€์กฑ์€ 507, ์ €์žฅ ์˜ค๋ฅ˜๋Š” 500์ž…๋‹ˆ๋‹ค. ์ ‘์ˆ˜ ์—ฌ๋ถ€๊ฐ€ ๋ถˆ๋ช…ํ™•ํ•˜๋ฉด ๊ทธ๋ฃน์„ ์ƒˆ๋กœ๊ณ ์นจํ•œ ๋’ค ์žฌ์š”์ฒญํ•˜์‹ญ์‹œ์˜ค. ํ™•์ •๋œ ์ถ”๊ฐ€๋Š” ํ˜„์žฌ ์ดํ•ฉ๊ณผ ๋ฐฐ์น˜ ๊ตฌ์„ฑ ์„œ๋ช…์„ ๋ฐ”๊พธ๋ฏ€๋กœ ๋‹ค์Œ ํ†ตํ•ฉ ๋ถ„์„์€ ๊ธฐ์กดยท์ถ”๊ฐ€ ์™„๋ฃŒ Run์„ ํ•จ๊ป˜ ์ง‘๊ณ„ํ•ฉ๋‹ˆ๋‹ค.

๋กœ์ปฌ ๊ทธ๋ฃน ๊ธฐ๋ก์ด ์›์ž์  ํ™•์ • ๊ฒฝ๊ณ„์ž…๋‹ˆ๋‹ค. ์ค€๋น„ ์ค‘์ธ ์ถ”๊ฐ€๋ถ„์€ ์ˆจ๊ธฐ๊ณ  ๋ณต๊ตฌ ์‹œ ๋กค๋ฐฑํ•˜๋ฉฐ, ํ™•์ •๋œ ๋Œ€๊ธฐ ํŒŒ์ผ์€ ๊ธฐ์กด ์žฌ์‹œ๋„ ์ ˆ์ฐจ๋กœ ์žฌ๊ฐœํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ๋ชฉ๋กยท์ƒˆ ๋ถ„์„์€ ๊ณผ๊ฑฐ ์›๋ณธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ณ  ํ˜„์žฌ ์ดํ•ฉ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฒฐ๊ณผ ์ €์žฅ์†Œ์™€ result-groups/<batchId>/batch.json์„ ํ•จ๊ป˜ ๋ฐฑ์—…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ €์žฅ ๊ฒฐ๊ณผ์™€ ๋‹ค์šด๋กœ๋“œ

๊ฒฐ๊ณผ API๋Š” ์‹œ์ž‘ ์‹œ๊ฐ ์ตœ์‹ ์ˆœ์„ ์œ ์ง€ํ•˜๋˜ queued์ฒ˜๋Ÿผ ์‹œ์ž‘ ์‹œ๊ฐ์ด ๊ฐ™์œผ๋ฉด ๋ฐฐ์น˜ยทํšŒ์ฐจยทID ์ˆœ์„œ๋กœ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค.

์›์‹œ ์ด๋ฒคํŠธ์™€ ๊ด€์ธก์€ ๋กœ์ปฌ <data-dir>/current-run/<run-id>/์— ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ๋ณด๊ด€ ์ž‘์—…์ž๋Š” ๊ฒ€์ฆํ•œ ๋กœ๊ทธ ์กฐ๊ฐ๊ณผ ํฐ ๊ฐœ๋ณ„ ๋ถ„์„ ํŒŒ์ผ์„ <data-dir>/runs/<run-id>/๋กœ ์˜ฎ๊ธฐ๊ณ  ์ž‘์€ ๋กœ์ปฌ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ธ๋ฑ์Šค๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ๋กœ์ปฌ data ๋””๋ ‰ํ„ฐ๋ฆฌ๋Š” data์ด๋ฉฐ Swarm์€ /var/lib/kpl/data์— ๋งˆ์šดํŠธํ•ฉ๋‹ˆ๋‹ค. NAS์—๋Š” runs/๋งŒ ๋‘˜ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณต์‚ฌยท๋Šฆ์€ ๋กœ๊ทธยท๋ณต๊ตฌยท๋ฐฑ์—… ๊ทœ์น™์€ ๋กœ์ปฌ ๊ธฐ๋ก๊ณผ NAS ๋ณด๊ด€์„ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

๋Œ€์‹œ๋ณด๋“œ์˜ Download results๋กœ ์‹คํ—˜ ๊ฒฐ๊ณผ๋ฅผ ZIP์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Saved results์—๋Š” ์ด์ „ Controller ์‹คํ–‰์—์„œ ๋ณด์กด๋œ ๊ฒฐ๊ณผ๋„ ํ‘œ์‹œ๋˜๋ฉฐ, Refresh๋กœ ๋ชฉ๋ก์„ ๋‹ค์‹œ ์ฝ์Šต๋‹ˆ๋‹ค. ์‹คํ–‰ ์ค‘ ์‹คํ—˜์˜ Download snapshot์€ ๋‹ค์šด๋กœ๋“œ ์‹œ์ž‘ ์‹œ์ ๊นŒ์ง€ ์ €์žฅ๋œ ๊ธฐ๋ก์„ ๋‹ด์Šต๋‹ˆ๋‹ค. ์ตœ๊ทผ 300๊ฐœ ์ด๋ฒคํŠธ ๋ฒ„ํผ์™€ ๋ณ„๊ฐœ๋กœ ์ €์žฅ๋œ ์ „์ฒด ์ด๋ฒคํŠธ ๋กœ๊ทธ๋ฅผ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. ํŒŒ์ผ ๊ตฌ์„ฑ๊ณผ ์ˆ˜์ง‘ ํ•œ๊ณ„๋Š” ์‹คํ—˜ ๊ฒฐ๊ณผ ๋‹ค์šด๋กœ๋“œ๋ฅผ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

GET /api/v1/results๋Š” ์ €์žฅ ๊ธฐ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ ๋ชฉ๋ก์˜ sourceBytes๋Š” scenario.yaml, experiment.json, events.jsonl, observations.jsonl, ์„ ํƒ์  note.json ์˜ ๋กœ์ปฌ ํŒŒ์ผ๊ณผ ์ธ๋ฑ์Šค์— ๋“ฑ๋ก๋œ NAS ๊ฐ์ฒด๋ฅผ ํ•ฉํ•œ ๋…ผ๋ฆฌ ํฌ๊ธฐ์ž…๋‹ˆ๋‹ค. NAS์— ์ ‘์†ํ•˜๊ฑฐ๋‚˜ ๋กœ๊ทธ ๋ณธ๋ฌธ์„ ์ฝ๊ณ  ๋ถ„์„ยท์••์ถ•ํ•˜์ง€ ์•Š๊ณ  ๋กœ์ปฌ ํŒŒ์ผ ํ†ต๊ณ„์™€ ๋ณด๊ด€ ์ธ๋ฑ์Šค๋งŒ ์กฐํšŒํ•˜๋ฏ€๋กœ ํฐ ๊ฒฐ๊ณผ๋„ ์šฉ๋Ÿ‰์„ ๋น ๋ฅด๊ฒŒ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ƒ์„ฑ๋œ ๋ถ„์„ ์บ์‹œยท์ด๋ฏธ์ง€, ๋‹ค์šด๋กœ๋“œ ์‹œ ์ƒ์„ฑํ•˜๋Š” metrics.jsonยทexport.json, ํŒŒ์ผ์‹œ์Šคํ…œ ํ• ๋‹น ์˜ค๋ฒ„ํ—ค๋“œ๋Š” ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ์†์ƒ๋˜์–ด๋„ ์›๋ณธ ํŒŒ์ผ ํฌ๊ธฐ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์œผ๋ฉด ์šฉ๋Ÿ‰์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์›๋ณธ ํŒŒ์ผ ์ž์ฒด์˜ ํ†ต๊ณ„๋ฅผ ์ฝ์„ ์ˆ˜ ์—†์œผ๋ฉด sourceBytes๋ฅผ ์ƒ๋žตํ•˜๊ณ  ํ™”๋ฉด์—๋Š” **Source ยท โ€”**๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.

DELETE /api/v1/results/{id}๋Š” ๋กœ์ปฌ ์‚ญ์ œ์™€ ์‚ญ์ œ ํ‘œ์‹ ์ €์žฅ ์„ฑ๊ณต ์‹œ 204(NAS ํŒŒ์ผ์€ ๋น„๋™๊ธฐ ์‚ญ์ œ), ๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด 404, ์‹คํ–‰ยท๋ฐฐ์น˜๊ฐ€ ํ™œ์„ฑ ์ƒํƒœ์ด๊ฑฐ๋‚˜ ์‹ค์ œ GET ๋‹ค์šด๋กœ๋“œ๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉ ์ค‘์ด๋ฉด 409๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋ชฉ๋ก ์กฐํšŒ๋Š” ๋‹ค์šด๋กœ๋“œ ์ถฉ๋Œ๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. NAS ํŒŒ์ผ descriptor๋ฅผ ์—ฌ๋Š” ์งง์€ ๋ณดํ˜ธ ๊ตฌ๊ฐ„์—๋Š” 409๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ HEAD๋Š” descriptor ์บก์ฒ˜ ํ›„ ์ด ๋ณดํ˜ธ๋ฅผ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค.

DELETE /api/v1/result-batches/{batchId}๋Š” ๋กœ๊ทธ์ธ ์„ธ์…˜๊ณผ ์ •ํ™•ํ•œ ๋ฐฐ์น˜ ID๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๊ธฐ ์ „์— ์ „์ฒด ๊ตฌ์„ฑ์›์„ ๊ฒ€์‚ฌํ•˜๋ฉฐ ์‹คํ–‰ยท์ •๋ฆฌ ์ค‘์ธ run์ด๋‚˜ ๊ตฌ์„ฑ์›์˜ ZIP ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์žˆ์œผ๋ฉด 409๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์‹คํŒจยท์ทจ์†Œ run์„ ํฌํ•จํ•œ ๋ชจ๋“  ์ €์žฅ ๊ตฌ์„ฑ์›, ๊ฐœ๋ณ„ ๋ถ„์„, ๋ณ„๋„ ํ†ตํ•ฉ ํ‰๊ท ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๊ณต ์‹œ 200๊ณผ {"deletedIds":["run-id", "..."]}๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ๊ทธ๋ฃน์ด ์—†์œผ๋ฉด 404์ž…๋‹ˆ๋‹ค. ์›๋ณธ ์—†์ด ๋‚จ์€ ํ†ตํ•ฉ ํ‰๊ท ๋„ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €์žฅ ์žฅ์น˜ ์˜ค๋ฅ˜๋Š” 500์ด๋ฉฐ ์ผ๋ถ€ ์‚ญ์ œ๊ฐ€ ๋๋‚ฌ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋ชฉ๋ก์„ ๊ฐฑ์‹ ํ•˜๊ณ  ์ €์žฅ ์˜ค๋ฅ˜๋ฅผ ํ•ด๊ฒฐํ•œ ๋’ค ์žฌ์‹œ๋„ํ•˜์‹ญ์‹œ์˜ค. ๋ธŒ๋ผ์šฐ์ €๋Š” ์š”์ฒญ์— 30์ดˆ ์ œํ•œ ์‹œ๊ฐ„์„ ์ ์šฉํ•˜๋ฉฐ ๊ทธ ์ดํ›„์—๋„ Controller์—์„œ ์™„๋ฃŒ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋Œ€์‹œ๋ณด๋“œ๋Š” ์šฉ๋Ÿ‰ ํ‘œ์‹œ๋ฅผ ์œ„ํ•œ HEAD ์š”์ฒญ์ด๋‚˜ ZIP ํฌ๊ธฐ ์žฌ์ธก์ • ํƒ€์ด๋จธ๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” HEAD /api/v1/experiments/RUN_ID/download๋Š” ๊ธฐ์กด์ฒ˜๋Ÿผ ZIP encoder๋ฅผ ๋ฐ”์ดํŠธ counter์— ์‹คํ–‰ํ•˜์—ฌ ์ •ํ™•ํ•œ Content-Length๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, ๊ฐœ๋ณ„ GET ๋‹ค์šด๋กœ๋“œ๋„ ํ•ด๋‹น ์ธก์ •์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ผ๊ด„ ๋‹ค์šด๋กœ๋“œ๋Š” ํฌ๊ธฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•˜์ง€ ์•Š๊ณ  ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ZIP์€ ๋ฉ”๋ชจ๋ฆฌ๋‚˜ ๋””์Šคํฌ์— ํ†ต์งธ๋กœ ๋ณด๊ด€ํ•˜์ง€ ์•Š๊ณ  ์ŠคํŠธ๋ฆฌ๋ฐํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ run์˜ ๋™์‹œ ์ธก์ •์€ ๊ณต์œ ํ•˜๊ณ  ์„œ๋กœ ๋‹ค๋ฅธ run์˜ ์ธก์ •์€ ์ตœ๋Œ€ ๋‘ ๊ฐœ๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด API ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด ๋ชฉ๋ก์€ ์ค€๋น„๋œ ZIP ํฌ๊ธฐ๊ฐ€ ์œ ํšจํ•˜๋ฉด downloadBytes๋„ ์ œ๊ณตํ•˜์ง€๋งŒ ํ™”๋ฉด์€ ์ด๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Pending publication์ด ์—†์œผ๋ฉด ํŒŒ์ผ๊ณผ ์ƒํƒœ๊ฐ€ ๊ฐ™์€ ๋™์•ˆ ์ธก์ •ํ•œ ๋ฐ”์ดํŠธ ์ˆ˜๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ HEAD ๋˜๋Š” ๋‹ค์šด๋กœ๋“œ๋งˆ๋‹ค ์ •๋ฐ€๋„๋ฅผ ์œ ์ง€ํ•œ ์ƒˆ export ์‹œ๊ฐ์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅ ๋ฐฉ์‹์ด ๊ณ ์ •๋œ export.json์—๋Š” ๊ณ ์ • ํญ ์‹œ๊ฐ ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์‹œ๊ฐ์ด ๋ฐ”๋€Œ์–ด๋„ ์ธ์ฝ”๋”ฉ ๊ธธ์ด๋Š” ๊ฐ™์Šต๋‹ˆ๋‹ค. Stable ๋ชฉ๋ก ํ–‰์—๋Š” downloadSizeMaxAgeMs๊ฐ€ ์—†๊ณ  stable HEAD ์‘๋‹ต์—๋Š” X-KPL-Result-Size-Max-Age-Ms๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. Pending publication์ด ์žˆ์œผ๋ฉด ๋ฐ”์ดํŠธ ์ˆ˜์™€ export ๊ฒฝ๊ณ„๋ฅผ ์งง๊ฒŒ ํ•จ๊ป˜ ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ชฉ๋ก ํ–‰์€ downloadSizeMaxAgeMs๋ฅผ ํฌํ•จํ•˜๊ณ  HEAD๋Š” X-KPL-Result-Size-Max-Age-Ms๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฐ’์€ ์„œ๋ฒ„์˜ ๋‚จ์€ cache ์œ ํšจ ์‹œ๊ฐ„์—์„œ ์‘๋‹ต ์•ˆ์ „ ์—ฌ์œ  1์ดˆ๋ฅผ ๋บ€ ์–‘์˜ ๋ฐ€๋ฆฌ์ดˆ์ด๋ฏ€๋กœ client์™€ server์˜ ์‹œ๊ณ„๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค. ์ค€๋น„๋œ ๊ฐ’์„ ์ฒ˜์Œ ํ™•์ธํ•œ ๋ชฉ๋ก๋งŒ ์œ ํšจ ์‹œ๊ฐ„์„ ํ•œ ๋ฒˆ ์—ฐ์žฅํ•˜๊ณ , ์—ฐ์žฅํ•œ ๊ฒฝ๊ณ„์—์„œ ๊ณ„์‚ฐํ•œ max age๋ฅผ ์‘๋‹ตํ•˜๋ฏ€๋กœ ๋ฐ”๋กœ ์ด์–ด์ง€๋Š” GET์ด ํ‘œ์‹œ ํฌ๊ธฐ์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. ๋ชฉ๋ก์„ ๋ฐ˜๋ณตํ•ด์„œ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ deadline์ด ๊ณ„์† ๋ฐ€๋ฆฌ์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ์•ˆ์ „ ์—ฌ์œ ๋ฅผ ์ œ์™ธํ•˜๊ณ  ๋‚จ์€ ์œ ํšจ ์‹œ๊ฐ„์ด ์—†์œผ๋ฉด ๋ชฉ๋ก์€ downloadBytes์™€ downloadSizeMaxAgeMs๋ฅผ ๋ชจ๋‘ ์ƒ๋žตํ•˜๊ณ , HEAD๋Š” ์‘๋‹ต ์ „์— ์ƒˆ ๊ฒฝ๊ณ„๋ฅผ ์ธก์ •ํ•ฉ๋‹ˆ๋‹ค.

Controller ์‹œ์ž‘ ํ›„ ์ฒซ HEAD๋‚˜ ์›๋ณธ ํŒŒ์ผยท์ƒํƒœ ๋ณ€๊ฒฝ ๋’ค์—๋Š” ์บก์ฒ˜ํ•œ ํŒŒ์ผ์„ ์ฝ๊ณ  ์ง€ํ‘œ๋ฅผ ๋‹ค์‹œ ๊ตฌ์„ฑํ•ด์•ผ ํ•˜๋ฉฐ, pending ๊ฒฐ๊ณผ๋Š” ์งง์€ cache๊ฐ€ ๋งŒ๋ฃŒ๋œ ๋’ค ์ด ์ž‘์—…์„ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค. ์ธก์ • slot ๋Œ€๊ธฐ, ์›๋ณธ ๋กœ๊ทธ ์ฝ๊ธฐ์™€ ์žฌ๊ตฌ์„ฑํ•œ ์ง€ํ‘œ ์ง‘๊ณ„ ๋‹จ๊ณ„์—์„œ HTTP ์š”์ฒญ ์ทจ์†Œ๋ฅผ ํ™•์ธํ•˜๋ฉฐ, ์ทจ์†Œ๋œ ์š”์ฒญ์€ server error ์‘๋‹ต ์—†์ด ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅํ•œ ์ด๋ฒคํŠธ ๋กœ๊ทธ๊ฐ€ ํฌ๋ฉด ์ธก์ • ์‹œ๊ฐ„์ด ๊ธธ์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋น„์šฉ์€ ์ง์ ‘ ์š”์ฒญํ•œ HEAD๋‚˜ ์‹ค์ œ ๋‹ค์šด๋กœ๋“œ์—๋งŒ ๋ฐœ์ƒํ•˜๋ฉฐ, ํ™”๋ฉด์˜ ์›๋ณธ ์šฉ๋Ÿ‰ ํ‘œ์‹œ์—๋Š” ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ZIP ์›๋ณธ ๋ณต์‚ฌ๋Š” ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋ฉฐ ์ง€ํ‘œ ์žฌ๊ตฌ์„ฑ์—๋Š” ๊ณ ์œ  ์ด๋ฒคํŠธ IDยท๋ฉ”์‹œ์ง€/์ˆ˜์‹  ์Œ์— ๋น„๋ก€ํ•œ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ผ๊ด„ ๋‹ค์šด๋กœ๋“œ

POST /api/v1/downloads๋Š” {"kind":"scenarios","ids":["scenario-id"]} ๋˜๋Š” {"kind":"results","ids":["run-id"]}๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ ID๋Š” ๋ฐฐ์น˜ ๊ตฌ์„ฑ์›์„ ํฌํ•จํ•œ ๊ฐœ๋ณ„ Run ID์ด๋ฉฐ ๋Œ€์‹œ๋ณด๋“œ๊ฐ€ ์„ ํƒํ•œ ๋ฐฐ์น˜๋ฅผ Run ๋ชฉ๋ก์œผ๋กœ ํ™•์žฅํ•ด ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜ ๋กœ๊ทธ์ธ ์„ธ์…˜๊ณผ X-KPL-Request: dashboard๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญ๋‹น ID 1~2,000๊ฐœ, JSON ๋ณธ๋ฌธ 512 KiB๊นŒ์ง€ ํ—ˆ์šฉํ•˜๋ฉฐ ์ค‘๋ณต ID๋Š” ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ฆ์€ ๋กœ์ปฌ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋งŒ ์ฝ๊ณ  ๋ณด๊ด€๋œ ์›๋ณธ ๋กœ๊ทธ๋Š” ์—ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์„ฑ๊ณตํ•˜๋ฉด 201๊ณผ {"url":"/api/v1/downloads/<ticket>","count":1,"expiresAt":"<UTC timestamp>"}๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ž˜๋ชป๋œ ์„ ํƒ์€ 400, ์—†๋Š” ํ•ญ๋ชฉ์€ 404, ์ฝ์„ ์ˆ˜ ์—†๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” 500, ๋Œ€๊ธฐ ํ‹ฐ์ผ“ ์ €์žฅ์†Œ๊ฐ€ ๊ฐ€๋“ ์ฐจ๋ฉด 429์ž…๋‹ˆ๋‹ค. ํ‹ฐ์ผ“์€ 10๋ถ„ ๋’ค ๋งŒ๋ฃŒ๋˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ์—๋งŒ ๋ณด๊ด€ํ•˜๋ฏ€๋กœ Controller ์žฌ์‹œ์ž‘ ์‹œ ๋ฌดํšจํ™”๋ฉ๋‹ˆ๋‹ค. ๋Œ€๊ธฐ ํ‹ฐ์ผ“์€ ์ตœ๋Œ€ 16๊ฐœ์ž…๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ ์ƒํƒœ์—์„œ ๋ฐ˜ํ™˜๋œ URL๋กœ GETํ•˜๋ฉด ํ‹ฐ์ผ“์„ ์†Œ๋ชจํ•˜๊ณ  scenarios-<UTC timestamp>.zip ๋˜๋Š” results-<UTC timestamp>.zip ์ฒจ๋ถ€ ํŒŒ์ผ์„ ์ŠคํŠธ๋ฆฌ๋ฐํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒด ZIP์„ ๋ธŒ๋ผ์šฐ์ € JavaScript๋‚˜ ์„œ๋ฒ„์— ๋ณด๊ด€ํ•˜์ง€ ์•Š๊ณ  ์‚ฌ์ „ ZIP ํฌ๊ธฐ ๊ณ„์‚ฐ๋„ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ผ๊ด„ ์ „์†ก์€ ์ตœ๋Œ€ ๋‘ ๊ฐœ๋ฅผ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ์ดˆ๊ณผ GET์€ ํ‹ฐ์ผ“์„ ์†Œ๋ชจํ•˜์ง€ ์•Š๊ณ  429์™€ Retry-After: 5๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ๋ฃŒยท์†Œ๋ชจ๋˜์—ˆ๊ฑฐ๋‚˜ ์—†๋Š” ํ‹ฐ์ผ“์€ 410์ž…๋‹ˆ๋‹ค. ๋‹ค์šด๋กœ๋“œ URL์€ ์ธ์ฆ์„ ๋Œ€์‹ ํ•˜์ง€ ์•Š์œผ๋ฉฐ HEAD๋Š” ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜ค ํ•ญ๋ชฉ์—๋Š” ์ €์žฅ๋œ YAML ์›๋ฌธ์„ ๋‹ด์Šต๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ ํ•ญ๋ชฉ์—๋Š” ๊ฐœ๋ณ„ Run ZIP์„ ๋‹ด์œผ๋ฉฐ ๊ฐ Run ์ฒ˜๋ฆฌ ์ฐจ๋ก€์— ์Šค๋ƒ…์ƒท์„ ํ™•๋ณดํ•ฉ๋‹ˆ๋‹ค. ํ‹ฐ์ผ“์€ ๋ชจ๋“  ์›๋ณธ์„ ๋ฏธ๋ฆฌ ๋ณดํ˜ธํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ฒ˜๋ฆฌ ์ „ยท๋„์ค‘์˜ ์‚ญ์ œ๋‚˜ ์ €์žฅ์†Œ ์˜ค๋ฅ˜๋กœ ์ „์†ก์ด ์‹คํŒจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ZIP ์ถœ๋ ฅ์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์—๋Š” ๋ˆ„๋ฝ ์‹œ 404, ์ฝ๊ธฐ ์‹คํŒจ ์‹œ 500์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์‹œ์ž‘ํ•œ ๋’ค์—๋Š” ์ŠคํŠธ๋ฆผ์„ ์ค‘๋‹จํ•˜๊ณ  ์™„์„ฑ๋œ ๋ฐ”๊นฅ ZIP์„ ๋‚จ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ทจ์†Œ ์‹œ ํ˜„์žฌ Run์˜ ๋‹ค์šด๋กœ๋“œ ๋ณดํ˜ธ์™€ ์ „์†ก ์Šฌ๋กฏ์„ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ๋ช…๊ณผ ๊ตฌ์„ฑ์€ ์‹œ๋‚˜๋ฆฌ์˜ค ๋‹ค์šด๋กœ๋“œ, ๊ฒฐ๊ณผ ๋‹ค์šด๋กœ๋“œ๋ฅผ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

๊ฒฐ๊ณผ ์ €์žฅ์†Œ ์ƒํƒœ

๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•œ GET /api/v1/result-storage๋Š” localDirectory, archiveDirectory, availableBytes, minFreeBytes, checking, checkStartedAt, lastCheckedAt, phase, lastProgressAt, stalled, ์„ ํƒ์  error๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ณต๊ฐ„์€ Controller์˜ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ํŒŒ์ผ์‹œ์Šคํ…œ์—์„œ ์ธก์ •ํ•˜๊ณ  NAS ์ƒํƒœ๋Š” ์ตœ๊ทผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ๊ฒฐ๊ณผ์ด๋ฏ€๋กœ ์š”์ฒญ์ด NAS ์กฐํšŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. phase๋Š” preparing, discovering, copying, verifying, publishing, deleting, ์‹คํ—˜ ์ค‘ ๋Œ€๊ธฐ์ธ paused, idle์„ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„๋Š” ์•„์นด์ด๋ธŒ I/O ๋‹จ๊ณ„์—์„œ ์ง„ํ–‰ ๊ธฐ๋ก์ด 15์ดˆ ๋„˜๊ฒŒ ๊ฐฑ์‹ ๋˜์ง€ ์•Š์„ ๋•Œ๋งŒ stalled๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒด ์ž‘์—… ์‹œ๊ฐ„, ๋กœ์ปฌ ์ค€๋น„, ์˜๋„์ ์ธ ์‹คํ—˜ ๋Œ€๊ธฐ๋Š” ์ง€์—ฐ ํŒ์ •์—์„œ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ๋ณต์‚ฌยท๊ฒ€์ฆ์€ ์ฒญํฌ ์™„๋ฃŒ๋งˆ๋‹ค ์ง„ํ–‰ ์‹œ๊ฐ์„ ๊ฐฑ์‹ ํ•˜๊ณ  ๋Œ€๊ธฐ ํ›„ ์žฌ๊ฐœํ•  ๋•Œ ํƒ€์ด๋จธ๋ฅผ ์ƒˆ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. UI๋Š” ์„œ๋ฒ„ ํŒ์ •์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋ธŒ๋ผ์šฐ์ € ์‹œ๊ณ„ ์ฐจ์ด๋กœ ๊ฒฝ๊ณ ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ง„ํ–‰ ์ง€์—ฐ ํ‘œ์‹œ์ด๋ฉฐ NAS ์„ฑ๋Šฅ ์ธก์ •์ด๋‚˜ ์ž๋™ ์ž‘์—… ์ทจ์†Œ๋ฅผ ์˜๋ฏธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹œ๋ณด๋“œ๋Š” ํ™”๋ฉด์ด ๋ณด์ผ ๋•Œ SSE์™€ ๋ณ„๋„๋กœ ์ด ์ž‘์€ endpoint๋ฅผ 15์ดˆ๋งˆ๋‹ค ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ ํ–‰์—๋Š” local, pending, archiving, archived ์ค‘ ํ•˜๋‚˜์ธ storage: {state, error?, updatedAt}๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅ ์ƒํƒœ๋Š” ์‹คํ—˜ ์ƒํƒœ๋‚˜ ๋ฐฐ์น˜ ํ‰๊ท  ๊ตฌ์„ฑ์„ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. sourceBytes๋Š” NAS ๋กœ๊ทธ ์กฐ๊ฐ๊ณผ ๋กœ์ปฌ ํ›„์† ๊ธฐ๋ก์„ ๊ฐ๊ฐ ํ•œ ๋ฒˆ๋งŒ ์…‰๋‹ˆ๋‹ค. ๋ณด๊ด€ํ•œ ์›๋ณธ ๋‹ค์šด๋กœ๋“œยท๋ถ„์„์—๋Š” NAS ์‘๋‹ต์ด ํ•„์š”ํ•˜์ง€๋งŒ ๋ชฉ๋กยท๋ฉ”๋ชจ๋Š” ๋กœ์ปฌ ์ธ๋ฑ์Šค๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด NAS ๊ฒฐ๊ณผ๋Š” ์‹œ์ž‘ ์‹œยท์ฃผ๊ธฐ์  ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ธ๋ฑ์Šค๊ฐ€ ๋ฐœ๊ฒฌํ•˜๋ฉด์„œ ํ™”๋ฉด์— ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

์ƒˆ POST /api/v1/experiments ์š”์ฒญ์€ ๋กœ์ปฌ ์—ฌ์œ ๊ฐ€ ์„ค์ • ๊ธฐ์ค€๋ณด๋‹ค ์ž‘์œผ๋ฉด 507์ž…๋‹ˆ๋‹ค. ๋ฐ˜๋ณตยท์žฌ๊ฐœ ๋ฐฐ์น˜๋Š” ๋‹ค์Œ Run ์ „์— phaseName: "Waiting for local result storage"์ธ ๋Œ€๊ธฐ ์ƒํƒœ๊ฐ€ ๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ‘œ์ค€ CLI ๊ธฐ๋ณธ๊ฐ’์€ --run-min-free-bytes / KPL_RUN_MIN_FREE_BYTES์˜ 1 GiB์ด๊ณ  0์ด๋ฉด ์—ฌ์œ  ๊ณต๊ฐ„ ๊ฒ€์‚ฌ๋ฅผ ๋•๋‹ˆ๋‹ค. ํ™œ์„ฑ Run์—์„œ๋„ ์•ฝ 1์ดˆ๋งˆ๋‹ค ๊ณต๊ฐ„ยท๊ธฐ๋ก ์˜ค๋ฅ˜๋ฅผ ํ™•์ธํ•˜๋ฉฐ ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ๋ฐ์ดํ„ฐ ๋ถˆ์™„์ „์„ ํ‘œ์‹œํ•˜๊ณ  ์‹คํ–‰ยท๋Œ€๊ธฐ ํšŒ์ฐจ๋ฅผ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ์ค€์€ ํฌ๊ธฐ๊ฐ€ ์ •ํ•ด์ง€์ง€ ์•Š์€ ํ™œ์„ฑ Run์˜ ์ „์ฒด ๊ณต๊ฐ„์„ ์˜ˆ์•ฝํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ ๋ฉ”๋ชจ

GET /api/v1/results/{id}/note๋Š” Cache-Control: no-store์™€ {runId, text, revision, updatedAt?}๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”๋ชจ๊ฐ€ ์—†์œผ๋ฉด text๋Š” ๋นˆ ๋ฌธ์ž์—ด, revision์€ "0"์ด๋ฉฐ ์‹œ๊ฐ์€ ์ƒ๋žตํ•ฉ๋‹ˆ๋‹ค. ์กฐํšŒยท์ €์žฅ ๋ชจ๋‘ ๋กœ๊ทธ์ธ ์„ธ์…˜์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. PUT์€ X-KPL-Request: dashboard์™€ ๋งˆ์ง€๋ง‰ ์กฐํšŒ์—์„œ ๋ฐ›์€ revision์„ ํฌํ•จํ•œ JSON {text, revision}์„ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๊ณตํ•˜๋ฉด 200๊ณผ ์ €์žฅ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ๋‚ด์šฉ์ด ๋ฐ”๋€Œ๋ฉด ์ƒˆ ๋ถˆํˆฌ๋ช… revision์„ ๋ฐœ๊ธ‰ํ•ฉ๋‹ˆ๋‹ค. updatedAt์€ UTC RFC 3339 ์‹œ๊ฐ์ž…๋‹ˆ๋‹ค. ์˜ค๋ž˜๋œ revision์€ 409๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ์ตœ์‹  ๋‚ด์šฉ์„ ์กฐํšŒยท๊ฒ€ํ† ํ•œ ํ›„ ๋‹ค์‹œ ์ œ์ถœํ•˜์‹ญ์‹œ์˜ค. ๋ฉ”๋ชจ๋Š” ์„ธ์…˜๋ณ„ ๋น„๊ณต๊ฐœ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹Œ ๊ณต์œ  ๊ธฐ๋ก์ž…๋‹ˆ๋‹ค.

๋ณธ๋ฌธ์€ ๋””์ฝ”๋”ฉ๋œ UTF-8 ๊ธฐ์ค€ ์ตœ๋Œ€ 16,384๋ฐ”์ดํŠธ์ž…๋‹ˆ๋‹ค. ๊ณต๋ฐฑ๋งŒ ์ž…๋ ฅํ•˜๋ฉด ๋ฉ”๋ชจ๋ฅผ ๋น„์šฐ๊ณ , ๊ทธ ์™ธ์—๋Š” ๊ณต๋ฐฑ๊ณผ ์ค„๋ฐ”๊ฟˆ์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ/null, ์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ, ํ›„ํ–‰ JSON๊ณผ ๋ณธ๋ฌธ ์šฉ๋Ÿ‰ ์ดˆ๊ณผ๋Š” 400์ž…๋‹ˆ๋‹ค. JSON ์ด์Šค์ผ€์ดํ”„์™€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ณ ๋ คํ•ด ์š”์ฒญ ์ „์ฒด๋Š” 6 * 16,384 + 4,096๋ฐ”์ดํŠธ๊นŒ์ง€ ํ—ˆ์šฉํ•˜๋ฉฐ ์ดˆ๊ณผ ์‹œ 413์ž…๋‹ˆ๋‹ค. ์ž˜๋ชป๋˜๊ฑฐ๋‚˜ ์—†๊ฑฐ๋‚˜ ์‚ญ์ œ๋œ run ID๋Š” 404์ž…๋‹ˆ๋‹ค. ์ €์žฅ์†Œ ์˜ค๋ฅ˜๋Š” 500์ด๋ฉฐ ์ฝ์„ ์ˆ˜ ์—†๋Š” ๊ธฐ์กด ๋ฉ”๋ชจ๋ฅผ ์ž„์˜๋กœ ๊ต์ฒดํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‚ด์šฉ์ด ๊ฐ™์œผ๋ฉด revision๊ณผ ์ €์žฅ ์‹œ๊ฐ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

Controller๋Š” ๊ฒฐ๊ณผ ์‚ญ์ œ์™€ ๋™์ผํ•œ ์ €์žฅ ์ž ๊ธˆ ์•„๋ž˜ ๋กœ์ปฌ current-run/<run-id>/note.json์„ ์›์ž์ ์œผ๋กœ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค. ๋ณด๊ด€ ์ž‘์—…์ž๋Š” ์ด ์ž ๊ธˆ ๋ฐ–์—์„œ runs/<run-id>/note.json์— ๋น„๋™๊ธฐ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๋น„์šด ๋ฉ”๋ชจ๋„ revision์„ ์œ ์ง€ํ•˜์—ฌ ์˜ค๋ž˜๋œ ํŽธ์ง‘์œผ๋กœ ๋‚ด์šฉ์ด ๋ณต์›๋˜์ง€ ์•Š๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ๊ฒฐ๊ณผ๋ฅผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•  ํ•„์š”๊ฐ€ ์—†๊ณ  ์‹คํ—˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋‚˜ ๋ถ„์„ ์บ์‹œ๋ฅผ ๋‹ค์‹œ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹คํ–‰ยท๋‹ค์šด๋กœ๋“œ ๋„์ค‘์—๋„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ด๋ฏธ ์บก์ฒ˜ํ•œ ZIP์€ ๋‹น์‹œ ๋ฉ”๋ชจ ๋ฒ„์ „์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. GET /api/v1/results๋Š” ๋น„์–ด ์žˆ์ง€ ์•Š์€ ๋ฉ”๋ชจ์— ์„ ํƒ์  note: {preview, updatedAt}๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋Š” ๊ณต๋ฐฑ์„ ์ •๋ฆฌํ•˜๊ณ  Unicode ์ฝ”๋“œ ํฌ์ธํŠธ 160๊ฐœ์™€ ๋ง์ค„์ž„ํ‘œ๋กœ ์ œํ•œํ•˜๋ฏ€๋กœ ์ „์ฒด ๋ณธ๋ฌธ์€ ๊ฐœ๋ณ„ ๋ฉ”๋ชจ API๋กœ ์กฐํšŒํ•˜์‹ญ์‹œ์˜ค. ๋Œ€์‹œ๋ณด๋“œ SSE์—๋Š” ๋ฉ”๋ชจ๊ฐ€ ๋“ค์–ด๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

GET / PUT /api/v1/result-batches/{batchId}/note๋Š” ๊ฐ™์€ ํฌ๊ธฐ ์ œํ•œยทrevisionยท์ƒํƒœ ์ฝ”๋“œยท๋กœ๊ทธ์ธ/CSRF ๊ทœ์น™์„ ์ ์šฉํ•˜๋ฉฐ {batchId, text, revision, updatedAt?}๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅ๋œ ๊ตฌ์„ฑ์›์ด๋‚˜ ๊ธฐ์กด ๊ทธ๋ฃน ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Group ๋ฉ”๋ชจ๋Š” ๋กœ์ปฌ result-groups/<batchId>/note.json์— ์ €์žฅํ•˜์—ฌ ์žฌ์‹œ๋„ยท๊ฐœ๋ณ„ Run ์‚ญ์ œยท๋ถ„์„ ๊ฐฑ์‹ ์œผ๋กœ ๋ฎ์–ด์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฃน ์ „์ฒด ์‚ญ์ œ๋Š” ๋ชจ๋“  ๊ตฌ์„ฑ์› ์‚ญ์ œ๊ฐ€ ์„ฑ๊ณตํ•œ ๋’ค ๋ฉ”๋ชจ๋ฅผ ์ œ๊ฑฐํ•˜๋ฉฐ, ๊ตฌ์„ฑ์›์ด ๋ชจ๋‘ ์‚ญ์ œ๋œ ๋ฉ”๋ชจ๋„ ๊ทธ๋ฃน ์‚ญ์ œ API๋กœ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ค๋ž˜๋œ ํŽธ์ง‘์ฐฝ์—์„œ ์ €์žฅํ•ด๋„ ์‚ญ์ œ๋œ ๊ทธ๋ฃน์„ ์žฌ์ƒ์„ฑํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ชฉ๋ก ์‘๋‹ต์€ ๊ฐ™์€ ๋‚ด์šฉ์„ ๋ฐ˜๋ณตํ•˜์ง€ ์•Š๋„๋ก ๊ทธ๋ฃน ๊ตฌ์„ฑ์› ์ค‘ ์ตœ๋Œ€ ํ•˜๋‚˜์— groupNote: {preview, updatedAt}๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” batchId๋กœ ๋ฌถ์€ ๋’ค ํ‘œ์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒด Group ๋ฉ”๋ชจ๋Š” ํŽธ์ง‘ ์‹œ์—๋งŒ ์กฐํšŒํ•˜๊ณ  SSE์™€ ๊ฐœ๋ณ„ Run ZIP์— ํฌํ•จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. NAS Run ๋ณด๊ด€์†Œ์—๋Š” ๋ฐ˜์˜ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ result-groups/๋ฅผ ๋กœ์ปฌ Controller ๋ฐ์ดํ„ฐ์™€ ํ•จ๊ป˜ ๋ฐฑ์—…ํ•˜์‹ญ์‹œ์˜ค.

๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ถ„์„

์ €์žฅ ์‹คํ–‰ ํ•˜๋‚˜๋ฅผ ๋ถ„์„ํ•˜๋ ค๋ฉด POST /api/v1/analysis-jobs/{id}๋กœ ์ ‘์ˆ˜ํ•˜๊ณ  ๊ฐ™์€ ๊ฒฝ๋กœ์˜ GET์œผ๋กœ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. POST์™€ GET/HEAD ๋ชจ๋‘ ๋กœ๊ทธ์ธ ์„ธ์…˜์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ ‘์ˆ˜๋œ ๋Œ€๊ธฐยท์‹คํ–‰ ์ž‘์—…์€ 202, ํ˜„์žฌ ๋ฒ„์ „์˜ ์™„๋ฃŒ ๊ฒฐ๊ณผ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ฉด 200์ž…๋‹ˆ๋‹ค. Controller ์ „์ฒด์—์„œ ๋Œ€๊ธฐยท์‹คํ–‰ ์ž‘์—…์„ ์ตœ๋Œ€ 32๊ฐœ ์ ‘์ˆ˜ํ•˜๊ณ (๊ฐ€๋“ ์ฐจ๋ฉด 503), ๊ณต์œ  ๋ถ„์„ ์Šฌ๋กฏ์€ ํ•œ ๋ฒˆ์— ํ•˜๋‚˜์˜ run์„ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋‹ซ์•„๋„ ์ ‘์ˆ˜๋œ ์ž‘์—…์€ ์ทจ์†Œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ•˜๋‚˜์˜ run ์•ˆ์—์„œ๋Š” ๋…๋ฆฝ๋œ ๊ด€์ธก ์Šค๋ƒ…์ƒท์˜ ๊ทธ๋ž˜ํ”„ ๋ฉ”ํŠธ๋ฆญ์„ ๋ณ‘๋ ฌ๋กœ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. worker ์ˆ˜๋Š” min(4, max(1, GOMAXPROCS - 1))์ด๋ฉฐ ์—ฌ๋Ÿฌ CPU๋ฅผ ์‚ฌ์šฉํ•˜๋Š” Controller์—์„œ๋Š” Go ์‹คํ–‰ ์Šฌ๋กฏ ํ•˜๋‚˜๋ฅผ ์—ฌ์œ ๋กœ ๋‚จ๊น๋‹ˆ๋‹ค. ๊ฐ™์€ ๊ทธ๋ž˜ํ”„๋Š” ์ง„ํ–‰ ์ค‘์ธ ๊ณ„์‚ฐ๊ณผ ์บ์‹œ๋ฅผ ๊ณต์œ ํ•˜๋ฉฐ ์ตœ๋Œ€ 256๊ฐœ๋ฅผ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๋Š” ์›๋ž˜ ๊ด€์ธก ์ˆœ์„œ๋ฅผ ์œ ์ง€ํ•˜๊ณ , phase: graph metrics ์™„๋ฃŒ/์ „์ฒด๋Š” ๊ณ„์‚ฐ์ด ๋๋‚œ ๊ด€์ธก ์ˆ˜๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๋‚˜ ์ทจ์†Œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋‚จ์€ worker๋ฅผ ์ค‘๋‹จํ•˜๊ณ  ์ข…๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ๋ฉ”ํŠธ๋ฆญ ์ •์˜์™€ ์บ์‹œ ๋ถ„์„ ๋ฒ„์ „์€ ์œ ์ง€ํ•˜๋ฉฐ, ๊ฐœ๋ณ„ยท๋ฐฐ์น˜ยท๋™๊ธฐ ๋ถ„์„์— ๋ชจ๋‘ ๊ฐ™์€ ๊ณ„์‚ฐ ๊ฒฝ๋กœ๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ƒํƒœ state๋Š” idle, queued, running, completed, failed, canceled, interrupted์ž…๋‹ˆ๋‹ค. idle์€ ํ•ด๋‹น ์ €์žฅ ์‹คํ–‰์— ์š”์ฒญํ•œ ๋ถ„์„์ด ์—†๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค. ์‹คํŒจยท์ทจ์†Œยท์ค‘๋‹จ ์ž‘์—…์€ ๋‹ค์‹œ ์ ‘์ˆ˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ์ž‘ ํ›„ ๋ณต๊ตฌํ•œ ๋ฏธ์™„๋ฃŒ ๊ธฐ๋ก์€ interrupted์ด๋ฉฐ ์ •์ƒ ์ข…๋ฃŒ ๊ณผ์ •์—์„œ ์ด๋ฏธ canceled๋ฅผ ์ €์žฅํ–ˆ์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณ„๋„ ์ž‘์—… ์ทจ์†Œ endpoint๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์‚ญ์ œ ๊ฐ€๋Šฅํ•œ ์ €์žฅ ๊ฒฐ๊ณผ๋ฅผ ์ง€์šฐ๋ฉด ๋ถ„์„ ์ž‘์—…๋„ ์ทจ์†Œํ•˜๊ณ  ๋ถ„์„ ํŒŒ์ผ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

์ƒํƒœ ํ•„๋“œ ์˜๋ฏธ
id, runId ๋ถ„์„ ์‹œ๋„ ID์™€ ์›๋ณธ ์‹คํ–‰ ID. ์žฌ์‹œ๋„ยท๊ฐฑ์‹ ์œผ๋กœ ์ƒˆ ์‹œ๋„๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์Œ
version, analysisVersion ์ƒํƒœยท์‘๋‹ต ํ˜•์‹ ๋ฒ„์ „ 1, ํ˜„์žฌ ์ž‘์—…์˜ ๊ณ„์‚ฐ ๋ฒ„์ „ 8. ์ง€ํ‘œ์˜ definition, ๋กœ๊ทธ์˜ rpcMetadataVersion๊ณผ ๋ณ„๊ฐœ
phase, processedBytes, totalBytes, progress ํ˜„์žฌ ๋‹จ๊ณ„์™€ ๋กœ๊ทธ ์ฝ๊ธฐ ๋ฐ”์ดํŠธ ์ง„ํ–‰๋ฅ (0โ€“100). ์ฝ๊ธฐ 100% ๋’ค์—๋„ ๊ทธ๋ž˜ํ”„ยท์ „ํŒŒ ๊ณ„์‚ฐ๊ณผ ์ €์žฅ์ด ๋‚จ์œผ๋ฏ€๋กœ state: completed๋ฅผ ํ™•์ธ
createdAt, startedAt, updatedAt, finishedAt ์ž‘์—… ์‹œ๊ฐ. ์•„์ง ๋„๋‹ฌํ•˜์ง€ ์•Š์€ ๋‹จ๊ณ„์˜ ์‹œ๊ฐ์—๋Š” ์˜๋ฏธ๊ฐ€ ์—†์Œ
sourceHash, sourceRevision, reused ์›๋ณธ ํ‘œ๋ณธ์˜ ํ•ด์‹œ, ๊ฐ€๋ฒผ์šด ๋ณ€๊ฒฝ ๊ฐ์ง€์šฉ ๊ฐ’, ํ•ด์‹œ ํ™•์ธ ํ›„ ๊ธฐ์กด ๋ถ„์„ ์žฌ์‚ฌ์šฉ ์—ฌ๋ถ€
savedAnalysisId ๊ฒ€์ฆยท์žฌ๋ถ„์„ ๋™์•ˆ ๋ณด์กดํ•˜๋Š” ์ด์ „ ์™„๋ฃŒ ๋ถ„์„ ID์ž…๋‹ˆ๋‹ค. ๊ต์ฒด ์ž‘์—… ์ค‘๋‹จยท์žฌ์‹œ์ž‘ ํ›„์—๋„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.
artifactSlot ์ž‘์—… ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ์„ ํƒํ•œ ์ €์žฅ ์Šฌ๋กฏ 0 ๋˜๋Š” 1์ž…๋‹ˆ๋‹ค. ์ƒ๋žตํ•˜๋ฉด 0์ด๋ฉฐ ๋ถ„์„ endpoint๊ฐ€ ํ•ด๋‹น ํŒŒ์ผ์„ ์ž๋™ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
snapshotAt POST ์ ‘์ˆ˜ ์‹œ์ ์ด ์•„๋‹ˆ๋ผ worker๊ฐ€ ๋ถ„์„ ์Šฌ๋กฏ์„ ์–ป์€ ๋’ค ์žก์€ ์›๋ณธ ๊ฒฝ๊ณ„
error, resultUrl ์‹คํŒจ ๋‚ด์šฉ ๋˜๋Š” ์‹œ๋„ ID๊ฐ€ ํฌํ•จ๋œ ์™„๋ฃŒ ์ „์ฒด ๋ถ„์„ URL

๋‹จ์ผยทํ†ตํ•ฉ ๋ถ„์„์€ phase: saving ๋™์•ˆ์—๋„ running ์ƒํƒœ์ด๋ฉฐ, ๊ฒฐ๊ณผ ํŒŒ์ผ์„ ์ €์žฅํ•˜๋Š” ์ค‘์— ์ƒํƒœ ์กฐํšŒยท์‹คํ—˜ ์ด๋ฒคํŠธ ์ˆ˜์‹ ยท์ค‘๋‹จ ์š”์ฒญ ์ฒ˜๋ฆฌ๋ฅผ ๊ณ„์†ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์™„๋ฃŒ ๊ฒฐ๊ณผ๋ฅผ ๊ฒŒ์‹œํ•˜๊ธฐ ์ „์— ํ˜„์žฌ ๋ถ„์„ ์‹œ๋„์ธ์ง€์™€ ์ทจ์†Œ ์—ฌ๋ถ€๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•˜๊ณ , ๊ฐœ๋ณ„ ๋ถ„์„์€ ์›๋ณธ ์‚ญ์ œ ์—ฌ๋ถ€๋„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

์„œ๋ฒ„๋Š” ์›๋ณธ์ด ๊ฐ™์•„๋„ ์™„๋ฃŒ ๋ถ„์„์˜ ๊ณ„์‚ฐ ๋ฒ„์ „์ด ํ˜„์žฌ ๋ถ„์„๊ธฐ์™€ ๋‹ค๋ฅด๋ฉด stale: true๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ๋ณ„ยท๋ฐฐ์น˜ ์ƒํƒœ์™€ Saved results ๋ชฉ๋ก ๋ชจ๋‘์— ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ๋ถ„์„๊ธฐ ๋ฒ„์ „ ๊ธฐ์ค€์„ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜์ง€ ์•Š๊ณ  ์ด ํ‘œ์‹œ๋กœ ๊ฐฑ์‹ ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ์™„๋ฃŒ ๋ถ„์„์€ ๊ต์ฒด ์ „๊นŒ์ง€ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด ๋Œ€๊ธฐยท์‹คํ–‰ ์ž‘์—…์€ ?refresh=1์ด์–ด๋„ ์žฌ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์™„๋ฃŒ ์ž‘์—…์˜ sourceRevision์€ ๊ฐ€๋ฒผ์šด ๋ณ€๊ฒฝ ๊ฐ์ง€์šฉ์ด๋ฉฐ ๋ชฉ๋กยท์ƒํƒœ ์กฐํšŒ์—์„œ NAS ๋กœ๊ทธ ๋ณธ๋ฌธ์„ ์ฝ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Controller์˜ ์•„์นด์ด๋ธŒ ์ด๋™์€ ์›๋ณธ ๋‚ด์šฉ์ด ๊ฐ™์œผ๋ฉด ๋กœ์ปฌ ๋กœ๊ทธ ๋ถ„๋ฆฌ ์ค‘์—๋„ revision์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๊ณ„์•ฝ์€ ์›๋ณธ ๋ณ€๊ฒฝ์„ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค. revision ๋ณ€๊ฒฝ ์‹œ ๊ฐœ๋ณ„ GET์€ completed, stale: true๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜ POST๋Š” ์ œํ•œ๋œ ์ž‘์—… ์Šฌ๋กฏ์—์„œ ๋…ผ๋ฆฌ์  ์›๋ณธ ๋ฐ”์ดํŠธ์˜ ํ‘œ๋ณธ์œผ๋กœ ๋งŒ๋“  SHA-256์ธ sourceHash๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ํ•ด์‹œ์™€ analysisVersion์ด ๊ฐ™์œผ๋ฉด ๊ธฐ์กด ์™„๋ฃŒ ์ž‘์—… IDยท๋ถ„์„ ํŒŒ์ผยท์Šค๋ƒ…์ƒท ์‹œ๊ฐ์„ ๋ณต์›ํ•˜๊ณ  revision์„ ๊ฐฑ์‹ ํ•˜๋ฉฐ reused: true๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ™•์ธ ์ž‘์—…์„ ํด๋งํ•˜๋‹ค ์™„๋ฃŒ๋˜๋ฉด ์›๋ž˜ ๋ถ„์„์˜ ID๋กœ ๋Œ์•„์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•ด์‹œ๊ฐ€ ๋‹ค๋ฅด๊ฑฐ๋‚˜ ๋ถ„์„๊ธฐ ๋ฒ„์ „์ด ์˜ค๋ž˜๋˜์—ˆ๊ฑฐ๋‚˜ ์ฝ˜ํ…์ธ  ํ•ด์‹œ๊ฐ€ ์—†๋Š” ์ด์ „ ๋ถ„์„์ด๋ฉด ๋‹ค์‹œ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. Images๊ฐ€ ์ด ๊ฒ€์‚ฌ๋ฅผ ์ž๋™ ์ˆ˜ํ–‰ํ•˜๋ฉฐ GET๋งŒ์œผ๋กœ ์ž‘์—…์„ ์‹œ์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Reanalyze๋Š” ?refresh=1๋กœ ์™„๋ฃŒ ๋ถ„์„์˜ ํ•ด์‹œ ์žฌ์‚ฌ์šฉ์„ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค. ์ƒˆ ๋ถ„์„์œผ๋กœ ๊ณผ๊ฑฐ์— ์—†๋˜ ๊ธฐ๋ก์„ ๋ณต์›ํ•  ์ˆ˜๋Š” ์—†์Šต๋‹ˆ๋‹ค.

ํ‘œ๋ณธ ํ•ด์‹œ ํ˜•์‹์€ sampled-sha256-v1:<hex>์ž…๋‹ˆ๋‹ค. ๋…ผ๋ฆฌ์  ์›๋ณธ(scenario.yaml, experiment.json, events.jsonl, observations.jsonl)๋งˆ๋‹ค ์ด๋ฆ„ยท์ „์ฒด ๋ฐ”์ดํŠธ ๊ธธ์ดยทํ‘œ๋ณธ ์œ„์น˜ยทํ‘œ๋ณธ ๋ฐ”์ดํŠธ๋ฅผ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. 64 KiB ์ดํ•˜๋Š” ์ „์ฒด๋ฅผ ์ฝ๊ณ , ๋” ํฐ ํŒŒ์ผ์€ ์•žยท๋’ค๋ฅผ ํฌํ•จํ•œ 512๋ฐ”์ดํŠธ ๊ตฌ๊ฐ„ 128๊ฐœ๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค. ๋‚ด๋ถ€ ์œ„์น˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ฒ„์ „ยท์ด๋ฆ„ยท๊ธธ์ด๋กœ ์žฌํ˜„ ๊ฐ€๋Šฅํ•œ ์˜์‚ฌ ๋‚œ์ˆ˜๋กœ ์ •ํ•˜๋ฏ€๋กœ ๋‹ค๋ฅธ ์Šค๋ƒ…์ƒท์ด๋‚˜ ์•„์นด์ด๋ธŒ ์กฐ๊ฐ ๋ฐฐ์น˜์—์„œ๋„ ๊ฐ™์€ ์œ„์น˜๋ฅผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. ์›๋ณธ๋ณ„ ์ตœ๋Œ€ 64 KiB๋ฅผ ์ž„์˜ ์ ‘๊ทผ์œผ๋กœ ์ฝ์œผ๋ฉฐ ๊ฒ€์ฆ์˜ ๋ฐ”์ดํŠธ ์ง„ํ–‰๋ฅ ์€ ์ด ์ฝ๊ธฐ ์–‘์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ๊ฐ™์€ ํฌ๊ธฐ์˜ ๋ณ€๊ฒฝ์ด ํ‘œ๋ณธ ๋ฐ–์— ์žˆ์œผ๋ฉด ํ•ด์‹œ๊ฐ€ ๊ฐ™์•„ ๋ณ€๊ฒฝ์„ ๋†“์น  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ „์ฒด ํŒŒ์ผ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ์ด ์•„๋‹Œ ๊ทผ์‚ฌ์ ์ธ ์บ์‹œ ํ™•์ธ์ž…๋‹ˆ๋‹ค. ๋ถ„์„ ๊ณ„์‚ฐ ์ž์ฒด๋Š” ํ•„์š”ํ•œ ์ž…๋ ฅ ์ „์ฒด๋ฅผ ์ฝ๊ณ  ?refresh=1์€ ๊ทธ ๊ณ„์‚ฐ์„ ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ์ „์ฒด ํ•ด์‹œ ๊ธฐ๋ฐ˜ ๋ถ„์„์€ ์›๋ณธ revision์ด ๊ฐ™์œผ๋ฉด ์บ์‹œ๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ, revision์ด ๋ฐ”๋€Œ๋ฉด ํ‘œ๋ณธ ๊ธฐ์ค€์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ํ•œ ๋ฒˆ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ๋ณด๊ด€ ๋ณต์‚ฌ์˜ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ๊ณผ ๋ณด์•ˆยท๋นŒ๋“œ ํ•ด์‹œ๋Š” ์ „์ฒด ๋‚ด์šฉ์„ ๊ณ„์† ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๊ฐœ๋ณ„ ์‹คํ–‰๊ณผ ๋ฐฐ์น˜ ํ‰๊ท  ๋ชจ๋‘ ์›๋ณธ ํ•ด์‹œ๋ฅผ ๋น„๊ตํ•˜๊ฑฐ๋‚˜ ์ƒˆ ๋ถ„์„์„ ๊ณ„์‚ฐํ•˜๋Š” ๋™์•ˆ ์ €์žฅ๋œ ๋ถ„์„์„ ๋จผ์ € ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ด์ „ ์Šค๋ƒ…์ƒท ์‹œ๊ฐ์„ ์œ ์ง€ํ•˜๋ฉฐ ๊ฒ€์ฆ ์ง„ํ–‰ ์ƒํ™ฉ์„ ๋ณ„๋„๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ํ•ด์‹œ๊ฐ€ ๊ฐ™์œผ๋ฉด ๊ทธ๋ž˜ํ”„ยท๊ฒ€์ƒ‰ ์ƒํƒœ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๊ณ  ์ƒˆ ๋ถ„์„์ด ์™„๋ฃŒ๋˜๋ฉด ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ฆ ์‹คํŒจ ์‹œ์—๋„ ๊ธฐ์กด ๊ฒฐ๊ณผ์™€ ์˜ค๋ฅ˜ ์•ˆ๋‚ด๋ฅผ ํ•จ๊ป˜ ๋‚จ๊น๋‹ˆ๋‹ค. ์ˆ˜๋™ ์žฌ๋ถ„์„๋„ ๋Œ€๊ธฐ ์ค‘ ํ˜„์žฌ ํ™”๋ฉด์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

๊ต์ฒด ์ž‘์—…์ด ์ง„ํ–‰ ์ค‘์ด๊ฑฐ๋‚˜ ์‹คํŒจํ•œ ์ƒํƒœ์—์„œ ์ด์ „ ๋ถ„์„์„ ์ฝ์„ ์ˆ˜ ์žˆ์œผ๋ฉด savedAnalysisId๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด /result ๋˜๋Š” ๊ฐœ๋ณ„ /summary ๊ฒฝ๋กœ์— ํ•ด๋‹น jobId์™€ &saved=1์„ ๋ช…์‹œํ•˜๋ฉด ์ด์ „ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜ ์š”์ฒญ์€ ๋ฏธ์™„๋ฃŒ ์ž‘์—…์— ์—ฌ์ „ํžˆ 409๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด์ „ ๊ฒฐ๊ณผ ์ฐธ์กฐ๋Š” Controller ์žฌ์‹œ์ž‘๊ณผ ํŒŒ์ผ ๊ฒŒ์‹œ ์‹คํŒจ์—๋„ ๋ณด์กดํ•˜๋ฉฐ ์™„๋ฃŒ๋œ ๊ต์ฒด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ฒŒ์‹œํ•  ๋•Œ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ถ„์„ ๋ฒ„์ „์„ ์˜๊ตฌ ๋ณด๊ด€ํ•˜๋Š” ๊ธฐ๋Šฅ์€ ์•„๋‹ˆ๋ฉฐ ๋ถˆ์ผ์น˜ยท๊ต์ฒด๋œ ID๋Š” 409์ž…๋‹ˆ๋‹ค. ํ™”๋ฉด์˜ ๋‹ค์šด๋กœ๋“œ ๋งํฌ๋Š” ํ‘œ์‹œ ์ค‘์ธ ๋ถ„์„ ID๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์™„๋ฃŒ ํ›„ GET ๋˜๋Š” HEAD /api/v1/analysis-jobs/{id}/result?jobId={jobId}๋Š” ์ „์ฒด JSON, /summary?jobId={jobId}๋Š” ๋น„๊ต์šฉ ์ž๋ฃŒ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. jobId๋ฅผ ์ง€์ •ํ•˜๋ฉด ๋‹ค๋ฅธ ๋ถ„์„ ์‹œ๋„๋ฅผ ๋‚ด๋ ค๋ฐ›์ง€ ์•Š๋„๋ก ํ™•์ธํ•˜๋ฉฐ ์ƒ๋žตํ•˜๋ฉด ํ˜„์žฌ ์™„๋ฃŒ ์‹œ๋„๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ๋ฏธ์™„๋ฃŒยท์‹œ๋„ ๋ถˆ์ผ์น˜๋Š” 409, ์‹คํ–‰ ๋ถ€์žฌ๋Š” 404, ์ž˜๋ชป๋˜๊ฑฐ๋‚˜ ์ฝ์„ ์ˆ˜ ์—†๋Š” ์ €์žฅ ์ž๋ฃŒ๋Š” ๋ณดํ†ต 422์ž…๋‹ˆ๋‹ค. ๊ฒฝ๋Ÿ‰ ์‘๋‹ต์€ observations, scoreTimeline, timeline, bandwidthTimeline, research.messages๋ฅผ ๋นˆ ๋ฐฐ์—ด๋กœ ๋‘๊ณ  messageCountยท์ง€ํ‘œยท์ง‘๊ณ„ยท๋ถ„ํฌยท์ ํ•ฉ ๊ฒฐ๊ณผ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ๋ณ„ ๋ฉ”์‹œ์ง€ ๊ฒฝ๋กœ๋‚˜ ์›๋ณธ ์‹œ๊ณ„์—ด ์šฉ๋„๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋‘ ์‘๋‹ต ๋ชจ๋‘ analysisId, analysisVersion, ์›๋ณธ ๊ฒฝ๊ณ„ asOf๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

scoreTimeline์€ ์ผ๋ฐ˜ ๊ด€์ธก ์ถ•์•ฝ๊ณผ ๋ณ„๋„๋กœ ์ƒ์„ธ ์ ์ˆ˜ ๊ตฌ์„ฑ์š”์†Œ๋ฅผ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฃนยท์ธก์ •์  ํ•„๋“œ, ํ•œ๋„์™€ ๊ฒฐ์ธก ๊ทœ์น™์€ ์ ์ˆ˜ ๊ตฌ์„ฑ์š”์†Œ ์‹œ๊ณ„์—ด์— ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ๋ณ„ ์ „์ฒด ๋ถ„์„๊ณผ ๋ฐฐ์น˜ ๋ถ„์„์— ๋ณด์กดํ•˜๊ณ  ๊ฐœ๋ณ„ ๋น„๊ต์šฉ ์š”์•ฝ์—์„œ๋Š” ๋นˆ ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

research.receiverGroups๋Š” ์ˆ˜์‹  ํ”ผ์–ด ๊ทธ๋ฃน(group; ๋นˆ ๋ฌธ์ž์—ด์€ ์ •๋ณด ๋ˆ„๋ฝยท์ถฉ๋Œ), ์ˆ˜์‹  ๋Œ€์ƒ ์ˆ˜, ๋ฉ”์‹œ์ง€ ์ˆ˜, ์š”์•ฝ ํ†ต๊ณ„, ๊ฒฝ๋Ÿ‰ overview, ์ „ํŒŒยท์ค‘๋ณตยทํ™‰ยทeager/lazy ๊ณก์„ ๊ณผ ๊ธฐ๋ณธ latencyCDF/latencyHistogram์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ง€์—ฐ ๋ถ„ํฌ๋Š” ๊ธฐ๋ณธ ์ง€ํ‘œ ์ •์˜๋ฅผ, ๋‚˜๋จธ์ง€ ๊ทธ๋ฃน ๊ณก์„ ์€ ์—ฐ๊ตฌ ์ง€ํ‘œ ์ •์˜๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฃน ์ž๋ฃŒ๋Š” ๋ฐฐ์น˜ ๊ฒฝ๋Ÿ‰ํ™” ํ›„์—๋„ ๋ณด์กด๋ฉ๋‹ˆ๋‹ค. ์ƒˆ TraceEvent์—๋Š” ์„ ํƒ ํ•„๋“œ group์ด ํฌํ•จ๋˜๋ฉฐ, ๊ณผ๊ฑฐ ๋ถ„์„์€ ํ™”๋ฉด์šฉ ๋‹ค์šด์ƒ˜ํ”Œ๋ง ์ „์— ์ „์ฒด ์ €์žฅ ๊ทธ๋ž˜ํ”„์—์„œ ๊ทธ๋ฃน์„ ๋ณต์›ํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜์‹  ๊ทธ๋ฃน๋ณ„ ์ง‘๊ณ„ ๊ธฐ์ค€์„ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

GET /api/v1/experiments/{id}/analysis๋Š” 2๋ถ„ ์š”์ฒญ ์ œํ•œ์˜ ๋™๊ธฐ ํ˜ธํ™˜ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค. ๋ณด์กด๋˜๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์„ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ์‘๋‹ต์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ๊ธด ๋ถ„์„๊ณผ ๋‚˜์ค‘ ๋‹ค์šด๋กœ๋“œ์—๋Š” job API๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์—ฐ๊ตฌ ์ •์˜๋Š” ์ง€ํ‘œ ๊ฐ€์ด๋“œ, ๋ณด์กด ํŒŒ์ผ์€ ๋ชจ๋‹ˆํ„ฐ๋ง, ๋ธŒ๋ผ์šฐ์ € ์กฐ์ž‘์€ ์‹œ๊ฐํ™”์—์„œ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ณต ์‹คํ—˜ ํ†ตํ•ฉ ๋ถ„์„

POST /api/v1/batch-analysis-jobs/{batchId}๋Š” ์‹คํ–‰ ์ œ์ถœ์— ๊ธฐ๋ก๋œ ๋™์ผ ๋ฐฐ์น˜์˜ ์™„๋ฃŒ run์„ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ฐฐ์น˜ ๊ตฌ์„ฑ์›์˜ ์‹คํ–‰ยท์ •๋ฆฌ ์™„๋ฃŒ์™€ ์ตœ์†Œ 2๊ฐœ์˜ ์™„๋ฃŒ run์ด ํ•„์š”ํ•˜๋ฉฐ, ์‹คํ–‰ ์ค‘์ด๋ฉด 409, ์œ ํšจ ์™„๋ฃŒ run ๋ถ€์กฑยท๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜๋Š” 422, ๋ฐฐ์น˜๊ฐ€ ์—†์œผ๋ฉด 404์ž…๋‹ˆ๋‹ค. ์ธ์ฆยท์ค‘๋ณต ์ ‘์ˆ˜ยท์žฌ์‹œ๋„ยท์žฌ์‹œ์ž‘ ์ฒ˜๋ฆฌ๋Š” ๊ฐœ๋ณ„ ๋ถ„์„๊ณผ ๊ฐ™๊ณ , ๊ฐœ๋ณ„ยท๋ฐฐ์น˜ ์ž‘์—…์ด ํ•ฉ์ณ์„œ ์ตœ๋Œ€ 32๊ฐœ์˜ ๋Œ€๊ธฐยท์‹คํ–‰ ์ž‘์—…์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ์ตœ๋Œ€ 100ํšŒ ๋ฐ˜๋ณต์„ ํ•˜๋‚˜์˜ ๋ฐฐ์น˜ ์ž‘์—…์œผ๋กœ ์ˆœ์ฐจ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์ƒํƒœ์˜ ๋ฐฐ์น˜ ์‹๋ณ„์ž๋Š” batchId์ด๋ฉฐ runId๋Š” ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์„ ํƒํ•œ runIds, completedRuns, totalRuns, expectedRuns, ๊ตฌ์„ฑ์› ์„œ๋ช… membership๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ณตํ†ต ์ž‘์—… ์Šฌ๋กฏ์—์„œ run์„ ํ•˜๋‚˜์”ฉ ์บก์ฒ˜ํ•ด ์›๋ณธ ํ•ด์‹œ๋ฅผ ๋จผ์ € ํ™•์ธํ•˜๋ฉฐ, ๋ฐ”์ดํŠธ ํ•„๋“œ๋Š” ํ˜„์žฌ ๋‹จ๊ณ„์™€ run์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ๊ณ„์‚ฐ ์ง„ํ–‰๋ฅ ์€ run๋ณ„ ๋™์ผ ๊ฐ€์ค‘์น˜์ž…๋‹ˆ๋‹ค. snapshotAt์€ ๊ฐ€์žฅ ์ตœ๊ทผ์— ๋ถ„์„ํ•œ run์˜ ๊ฒฝ๊ณ„์ด๋ฉฐ runs[].asOf๋Š” ๊ฐœ๋ณ„ ๊ฒฝ๊ณ„์ž…๋‹ˆ๋‹ค. ๊ตฌ์„ฑ์›ยท์ƒํƒœยท์›๋ณธ revision ๋ณ€๊ฒฝ ์‹œ ์ด์ „ ์™„๋ฃŒ ์บ์‹œ๋ฅผ idle, stale: true๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜ POST๋Š” ์™„๋ฃŒ run์˜ ํ‘œ๋ณธ ํ•ด์‹œ, ๊ตฌ์„ฑ์› ์‹๋ณ„์žยท๊ฒฐ๊ณผ, ๊ณผ๊ฑฐ ์‹œ๋„ ์ง‘๊ณ„๋ฅผ ๋ฐ˜์˜ํ•œ ๋ฐฐ์น˜ sourceHash๋ฅผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์œผ๋ฉด ์›๋ž˜ ๋ฐฐ์น˜ ๋ถ„์„ ํŒŒ์ผ๊ณผ ID๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ณ  ๋‹ค๋ฅด๋ฉด ๋‹ค์‹œ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ์ œ์™ธ๋œ ์ด๋ฒคํŠธ ๋กœ๊ทธ์™€ ๋ฉ”๋ชจ๋Š” ํ†ต๊ณ„ ์ž…๋ ฅ์ด ์•„๋‹™๋‹ˆ๋‹ค. ๋กœ์ปฌ revision์€ ์ œ๊ณต๋˜๋Š” ํŒŒ์ผ ์‹๋ณ„์žยท๋ณ€๊ฒฝ ์‹œ๊ฐ๋„ ๋ฐ˜์˜ํ•˜๋ฏ€๋กœ ํฌ๊ธฐยทmtime์„ ์œ ์ง€ํ•œ ์žฌ์ž‘์„ฑ๋„ ํ•ด์‹œ ํ™•์ธ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ์•„์นด์ด๋ธŒ revision์€ NAS ๋ณธ๋ฌธ ๋Œ€์‹  manifest ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. NAS ๊ฐ์ฒด๋Š” ๋ถˆ๋ณ€์œผ๋กœ ์ทจ๊ธ‰ํ•˜๋ฉฐ ์ €์žฅ ์›Œํฌํ”Œ๋กœ๋ฅผ ํ†ตํ•ด ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ?refresh=1์€ ๊ฐ•์ œ๋กœ ๋‹ค์‹œ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ํ•ด์‹œ ํ™•์ธยท๊ณ„์‚ฐยท์ €์žฅ ์ค‘ ๊ตฌ์„ฑ์›์ด๋‚˜ ์›๋ณธ revision์ด ๋ฐ”๋€Œ๋ฉด ์ž‘์—…์„ ์‹คํŒจ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ํ˜„์žฌ ์›๋ณธ์œผ๋กœ ์žฌ์‹œ๋„ํ•˜์‹ญ์‹œ์˜ค.

๊ตฌ์„ฑ์› ์„œ๋ช…์—๋Š” ํ˜„์žฌ ์™„๋ฃŒ ๊ธฐ์—ฌ Run์˜ ์›๋ณธ revision๋งŒ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. ์ œ์™ธ๋œ ์‹œ๋„์™€ ์ด์ „ ์‹œ๋„์˜ ์‹๋ณ„์žยท๊ฒฐ๊ณผ๋Š” ๊ณ„์† ๋ฐ˜์˜ํ•˜์ง€๋งŒ, ํ†ต๊ณ„์— ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋กœ๊ทธ ๋ณ€๊ฒฝ์œผ๋กœ ๋ฐฐ์น˜ ํ‰๊ท ์˜ ์›๋ณธ ๊ฒ€์ฆ์„ ์˜ˆ์•ฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ†ตํ•ฉ ๊ฒฐ๊ณผ์˜ reliability๋Š” attempts, failed, canceled, interrupted, retries, failedAttemptRate, unknownAttempts, unverifiedRuns, incompleteRuns, environments, environmentChanged, warnings๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ์‹œ์ž‘ํ•œ ์‹œ๋„์™€ ์‚ญ์ œ ํ›„ ๋ณด์กด๋œ ์‹œ๋„ ์ด๋ ฅ์„ ์ง‘๊ณ„ํ•˜๋ฉฐ ๋ฏธ์‹œ์ž‘ ๋Œ€๊ธฐ ๊ธฐ๋ก์€ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ์‹คํŒจ์œจ์€ failed / attempts์ด๊ณ  ์ง€ํ‘œ ํ‰๊ท ์€ ๊ณ„์† ์ตœ์‹  ์™„๋ฃŒ ํšŒ์ฐจ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ™˜๊ฒฝ๋ณ„ fingerprint, Run ID, Agent ์Šค๋ƒ…์ƒท๊ณผ Controller ๋นŒ๋“œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜์ง€๋งŒ ํ˜ธ์ŠคํŠธ ๋ถ€ํ•˜ยท๋ฏธ๊ธฐ๋ก ๋„คํŠธ์›Œํฌ ์ฐจ์ด๋Š” fingerprint ๋ฒ”์œ„ ๋ฐ–์ž…๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ๋Š” version: 1, analysisVersion: 8, aggregation: "equal-run-mean-v1", ๋ฐฐ์น˜ยท์‹œ๋„ ID, expectedRuns, missingRuns, excluded, runs, summary๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. summary๋Š” metrics.*, research.*, bandwidth.sentBytes, bandwidth.receivedBytes๋ณ„ average, deviation(run ๊ฐ„ ํ‘œ๋ณธ SD), median, count(์œ ํšจ run ์ˆ˜)์ž…๋‹ˆ๋‹ค. ๊ฒฐ์ธก ํ‰๊ท ์€ null, ์œ ํšจ run 1๊ฐœ์˜ SD๋„ null์ž…๋‹ˆ๋‹ค. runs๋Š” ๊ทธ๋ž˜ํ”„ ์ง€ํ‘œยท๊ด€์ธกยท๋Œ€์—ญํญยท๋ถ„ํฌ์™€ ๋ฉ”์‹œ์ง€ ์‹œ๊ณ„์—ด/์ˆ˜์‹  ๊ณก์„ /๊ธฐ์› ์ˆ˜๋ฅผ ๋‹ด์€ research.overview๋ฅผ ๋ณด์กดํ•˜๊ณ  ์›์‹œ ๋ฉ”์‹œ์ง€ยท๋…ธ๋“œ ๊ฒฝ๋กœ๋ฅผ ์ƒ๋žตํ•ฉ๋‹ˆ๋‹ค. ํ‰๊ท  ์ฐจํŠธ์˜ ์ถ• ์ •๋ ฌยท๋ถ„ํฌ ๊ทœ์น™์€ ํ†ตํ•ฉ ํ‰๊ท  ์‚ฌ์šฉ๋ฒ•์„ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

์™„๋ฃŒ ํŒŒ์ผ์€ <data-dir>/batch-analyses/{batchId}/job.json, result.json์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ์ฐฝ ์ข…๋ฃŒ์™€ ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ํ›„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ช…์‹œ์ ์ธ jobId ๋‹ค์šด๋กœ๋“œ๋Š” ํ•ด๋‹น ์™„๋ฃŒ ์Šค๋ƒ…์ƒท์— ๊ณ ์ •๋˜๋ฉฐ ๋‹ค๋ฅธ ์‹œ๋„๋กœ ๊ต์ฒด๋์œผ๋ฉด 409์ž…๋‹ˆ๋‹ค. ๊ฐœ๋ณ„ ์›๋ณธ ์‚ญ์ œ๋Š” ๊ธฐ์กด ํ†ตํ•ฉ ์Šค๋ƒ…์ƒท ํŒŒ์ผ์„ ์ง€์šฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ๋œ ๊ตฌ์„ฑ์œผ๋กœ ๋‹ค์‹œ ๋ถ„์„ํ•˜๋ฉด ํ˜„์žฌ ํŒŒ์ผ์„ ๊ต์ฒดํ•˜๋ฏ€๋กœ ์ด์ „ ๊ฒฐ๊ณผ๊ฐ€ ํ•„์š”ํ•˜๋ฉด ๋จผ์ € ๋‹ค์šด๋กœ๋“œํ•˜์‹ญ์‹œ์˜ค.

์‹คํ—˜ ๋„คํŠธ์›Œํฌ ์ค€๋น„

์ œ๊ณต๋˜๋Š” Swarm stack์€ Controller์˜ --network-manager(KPL_NETWORK_MANAGER_URL)์™€ Agent์˜ --auto-reset-network --control-network NAME์œผ๋กœ ์ด ๊ฒฝ๋กœ๋ฅผ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ด ์˜ต์…˜์„ ์ƒ๋žตํ•œ ์‚ฌ์šฉ์ž ์ง€์ • ๋ฐฐํฌ๋Š” ์ด์ „ ๋„คํŠธ์›Œํฌ ์ˆ˜๋ช…์ฃผ๊ธฐ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. Stack์€ ์ œ์–ด ์„œ๋น„์Šค๋ฅผ ๊ณ ์ • overlay์— ๋ฐฐ์น˜ํ•˜๊ณ  control manager์˜ Docker socket๊ณผ ์˜์† ๋ณต๊ตฌ volume์„ ์‚ฌ์šฉํ•˜๋Š” network-manager๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. Standalone gateway์—๋Š” Docker socket์ด๋‚˜ ํ˜ธ์ŠคํŠธ ๊ฒฝ๋กœ๋ฅผ ๋งˆ์šดํŠธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐฐํฌยท์—…๊ทธ๋ ˆ์ด๋“œ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

Controller๋Š” ๋งค Run ์ „์— ๋น„ํ™œ์„ฑ Agent๋ฅผ ํฌํ•จํ•œ ๋“ฑ๋ก Agent ๋ชจ๋‘์— POST /api/v1/network/fence๋ฅผ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋ณธ๋ฌธ์€ {runId, epoch, requestedAt}์ž…๋‹ˆ๋‹ค. Agent๋Š” ๊ธฐ์กด ํ”„๋กœ์„ธ์Šค ์ œ๊ฑฐ๊ฐ€ ํ™•์ธ๋œ ๊ฒฝ์šฐ์—๋งŒ ์ƒˆ ์ž…์žฅ์„ ๋ง‰๊ณ  Docker์— ๊ด€๋ฆฌ ๋Œ€์ƒ Peer ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ๋‚จ์•˜๋Š”์ง€๋„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๊ฑฐ์ ˆํ•˜๊ฑฐ๋‚˜ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š๋Š” Agent๊ฐ€ ์žˆ์œผ๋ฉด ์ค€๋น„๊ฐ€ ์ค‘๋‹จ๋ฉ๋‹ˆ๋‹ค. ์ดํ›„ ๊ฐ™์€ ์‹๋ณ„ ์ •๋ณด์™€ Agent IDโ†’์ œ์–ด URL์˜ agents ๋งต์„ Manager์˜ POST /api/v1/network/prepare๋กœ ๋ณด๋ƒ…๋‹ˆ๋‹ค. Manager๋Š” ์ค€๋น„๋ฅผ ์ง๋ ฌํ™”ํ•˜๊ณ  ์˜ค๋ž˜๋œ ์š”์ฒญ์„ ๊ฑฐ์ ˆํ•˜๋ฉฐ Swarm ๋…ธ๋“œ Ready, ๋„คํŠธ์›Œํฌ ์†Œ์œ ๊ถŒยท๊ธฐ๋ณธ IPv4 ์„ค์ •ยท์„œ๋น„์Šค ์ฐธ์กฐ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. Subnet๊ณผ gateway๋ฅผ ๋ณด์กดํ•˜๋ฉด์„œ ์ƒˆ network ID๋กœ ์ƒ์„ฑํ•˜๋ฉฐ ๋„คํŠธ์›Œํฌ ๊ฐ•์ œ ์‚ญ์ œ๋Š” ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‚ญ์ œ ํ›„ ์ƒ์„ฑ์ด๋‚˜ ์‘๋‹ต์ด ์‹คํŒจํ•ด๋„ ๋กœ์ปฌ ๋ณต๊ตฌ ํŒŒ์ผ์— ๊ธฐ์กด ํ• ๋‹น ์„ค์ •์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค.

Docker ์‚ญ์ œ ์„ฑ๊ณต ์‘๋‹ต ์ดํ›„ Manager๋Š” ํ†ตํ•ฉ network ๋ชฉ๋ก์—์„œ ์ •ํ™•ํ•œ Peer network ์ด๋ฆ„์ด ์‚ฌ๋ผ์ง„ ๊ฒƒ์„ ํ™•์ธํ•œ ๋’ค ๊ต์ฒด ๋„คํŠธ์›Œํฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์‚ญ์ œ ๋ฐ˜์˜ ๋Œ€๊ธฐ, ๋ช…์‹œ์ ์ธ network with name ... already exists ๊ฑฐ์ ˆ์˜ ์žฌ์‹œ๋„, ์ƒ์„ฑ ์„ฑ๊ณต ํ›„ ๋ชฉ๋ก ๋ฐ˜์˜ ๋Œ€๊ธฐ๋Š” ๊ธฐ์กด Manager ์ž‘์—… ์ œํ•œ ์•ˆ์—์„œ ํ•ฉ๊ณ„ 1๋ถ„์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ํด๋ง์€ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๊ณ  ์กฐํšŒ ์‹คํŒจ๋ฅผ ์‚ญ์ œ ํ™•์ธ์œผ๋กœ ์ทจ๊ธ‰ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‚ญ์ œ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋˜ ์ค‘ ๋‹ค๋ฅธ ID๊ฐ€ ๋‚˜ํƒ€๋‚˜๋ฉด ์ค€๋น„๋ฅผ ์ค‘๋‹จํ•˜๋ฉฐ ํ•ด๋‹น ๋„คํŠธ์›Œํฌ๋ฅผ ์‚ญ์ œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ƒ์„ฑ ์‘๋‹ต์ด ์‹คํŒจํ•˜๋ฉด inspect๋กœ ์‹ค์ œ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ์ƒˆ ๋„คํŠธ์›Œํฌ๊ฐ€ ํ˜„์žฌ epochยท์˜ˆ์ƒ ์†Œ์œ ๊ถŒยทํ• ๋‹น ์„ค์ •๊ณผ ์ผ์น˜ํ•˜๊ณ  endpoint ๋ฐ ์„œ๋น„์Šค ์ฐธ์กฐ๊ฐ€ ์—†์„ ๋•Œ๋งŒ ํ›„์† ์ค€๋น„๋ฅผ ๊ณ„์†ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ์™ธ์—๋Š” ๋ช…์‹œ์ ์ธ ์ด๋ฆ„ ์ถฉ๋Œ๋งŒ ์ƒํƒœ๋ฅผ ๋‹ค์‹œ ํ™•์ธํ•œ ๋’ค ์žฌ์‹œ๋„ํ•˜๋ฉฐ, ์ผ๋ฐ˜ ์˜ค๋ฅ˜๋Š” ์ƒ์„ฑ ์žฌ์‹œ๋„๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์›๋ž˜ network ID๋ฅผ ์ƒˆ ๋„คํŠธ์›Œํฌ๋กœ ์ˆ˜๋ฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ชฉ๋ก ๋ฐ˜์˜ ์ง€์—ฐ, ์ง€์†์ ์ธ ์ถฉ๋Œ, ์ทจ์†Œ ๋˜๋Š” ํ™•์ธ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๊ฐ€ ํ•ด์†Œ๋˜์ง€ ์•Š์œผ๋ฉด ํ™œ์„ฑํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ๋Œ€๊ธฐ ์‹œ๊ฐ„์—๋Š” ์‹คํ—˜ยท์‹œ๋‚˜๋ฆฌ์˜ค schedule ์‹œ๊ณ„๊ฐ€ ์ง„ํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์‘๋‹ต์€ {runId, epoch, networkId, networkName, gatewayUrl, peerGatewayUrl}์ž…๋‹ˆ๋‹ค. Controller๋Š” ์ด๋ฅผ ๊ฐ Agent์˜ POST /api/v1/network/activate๋กœ ๋ณด๋‚ด๊ณ  ๋ชจ๋‘ ํ™•์ธ๋œ ๋’ค Run์˜ startedAt์„ ๊ธฐ๋กํ•˜๊ณ  phase๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. Experiment.peerNetworkId์— ์ƒˆ ์‹๋ณ„์ž๋ฅผ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. CreateNodeRequest.peerNetworkId๋Š” Agent๊ฐ€ ์ค€๋น„ํ•œ ํ˜„์žฌ Runยท์‹œ๋„์˜ ์ž…์žฅ ์กฐ๊ฑด๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•˜๋ฉฐ Docker ์ƒ์„ฑ์—๋Š” ๊ณ ์ • ID๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Worker๋Š” ์ฒซ ์ปจํ…Œ์ด๋„ˆ ์—ฐ๊ฒฐ ๋•Œ overlay๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ Agent๋Š” ์‹œ์ž‘ ํ›„ ์‹ค์ œ ์—ฐ๊ฒฐ์˜ network ID๋„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ๋Šฆ์€ ์ƒ์„ฑ ์š”์ฒญ์ด ๊ฐ™์€ ์ด๋ฆ„์˜ ๊ต์ฒด ๋„คํŠธ์›Œํฌ์— ์ž…์žฅํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ค€๋น„ ์‹คํŒจ ์‹œ phase๋ฅผ ์‹œ์ž‘ํ•˜์ง€ ์•Š๊ณ  ๊ธฐ์กด ์‹คํŒจยท์ •๋ฆฌยท์˜ˆ์•ฝ ์ฒ˜๋ฆฌ ๊ทœ์น™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

์ค€๋น„ endpoint์—๋Š” ๋‚ด๋ถ€ Bearer ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ endpoint๋Š” JSON ๊ฐ’ ํ•˜๋‚˜๋งŒ ํ—ˆ์šฉํ•˜๊ณ  ์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ๋ฅผ ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. Agent ๋ณธ๋ฌธ์€ 64 KiB, Manager ๋ณธ๋ฌธ์€ 1 MiB ํ•œ๋„์ž…๋‹ˆ๋‹ค. Agent๋Š” ํ™•์ธ ์‹œ 204, ์ž˜๋ชป๋œ ์ž…๋ ฅ์€ 400, ์ค€๋น„ยท์ •๋ฆฌยทํ™œ์„ฑํ™”๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†์œผ๋ฉด 409๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Manager๋Š” ์„ฑ๊ณตํ•œ JSON ์‘๋‹ต์— 200, ์ž˜๋ชป๋œ ๋ณธ๋ฌธ์— 400, ๊ฑฐ์ ˆยท์‹คํŒจํ•œ ์ค€๋น„์— 409๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ์ธ์ฆ ์‹คํŒจ๋Š” 401์ž…๋‹ˆ๋‹ค. ์ „์ฒด ์ค€๋น„ ์ œํ•œ์€ 4๋ถ„์ด๋ฉฐ Manager ์ž‘์—…์€ 3๋ถ„, Agent ํ˜ธ์ถœ์€ 45์ดˆ๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. Gateway๋Š” ์ธ์ฆ๋œ HTTP ์ œ์–ดยทtelemetry์™€ ๊ณต๊ฐœ Controller ์‹œ๊ฐ health ์š”์ฒญ์„ overlay ์‚ฌ์ด์—์„œ ์ „๋‹ฌํ•˜๋ฉฐ Peer์˜ P2P ํ†ต์‹ ์€ ์ง์ ‘ ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. Gateway ์ž์›์€ ์ œ์–ด ์„œ๋น„์Šค ๋ชจ๋‹ˆํ„ฐ๋ง์— ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. Gateway ๊ต์ฒด๋Š” ๋‹ค์Œ ์ •๋ฆฌ ํ™•์ธ ๋‹จ๊ณ„๊นŒ์ง€ ๋ฏธ๋ฃน๋‹ˆ๋‹ค. ์„œ๋น„์Šค ์ค‘์ง€ยทDocker ์žฌ์‹œ์ž‘ยท๋ฐ์ดํ„ฐ volume ๋ณ€๊ฒฝ์€ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์šด์˜ ์—ฐ๊ฒฐ์„ฑ๊ณผ ๋Œ€๊ทœ๋ชจ ๋ฐ˜๋ณต ์‹คํ–‰์€ ๋ณ„๋„ ๋ฐฐํฌ ๊ฒ€์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๋ถ€ cluster endpoint

์ด REST/JSON endpoint๋Š” ์ปดํฌ๋„ŒํŠธ ์‚ฌ์ด ํ†ต์‹ ์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋“ฑ๋ก, heartbeat์™€ ์ „๋‹ฌ event๋Š” Controller๋กœ, lifecycle ๋ช…๋ น์€ Agent๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

ํ˜ธ์ถœ์ž โ†’ ์„œ๋ฒ„ Method Path ๋ชฉ์ 
Agent โ†’ Controller POST /api/v1/agents/register Agent instance ๋“ฑ๋ก
Agent โ†’ Controller POST /api/v1/agents/heartbeat Agent์™€ Peer snapshot ๋ณด๊ณ 
Agent โ†’ Controller POST /api/v1/events/batch batch๋‹น ์ตœ๋Œ€ 5000๊ฐœ event ์ „๋‹ฌ
Controller โ†’ Agent GET /api/v1/status Agent์™€ Peer ์ƒํƒœ ๊ฐฑ์‹ 
Controller โ†’ Agent POST /api/v1/registration/refresh ์„ค์ •๋œ Controller์— ์ธ์ฆ๋œ ์žฌ๋“ฑ๋ก ํ›„ AgentยทPeer ์ƒํƒœ ๋ฐ˜ํ™˜; Peer ์žฌ์‹œ์ž‘ ์—†์Œ
Controller โ†’ Agent POST /api/v1/nodes CreateNodeRequest๋กœ Peer ์ƒ์„ฑ
Controller โ†’ Agent DELETE /api/v1/nodes ๊ธฐ์กด run์„ fence ์ฒ˜๋ฆฌํ•˜๊ณ  ๋‚จ์€ Peer์™€ ๋Œ€๊ธฐ telemetry๋ฅผ ๋ชจ๋‘ ์ •๋ฆฌํ•œ ๋’ค 204 ๋ฐ˜ํ™˜. Controller ์ข…๋ฃŒ ์‹œ ์‚ฌ์šฉ
Controller โ†’ Agent DELETE /api/v1/nodes/{nodeId} Peer ํ•˜๋‚˜์˜ ์ข…๋ฃŒ ์š”์ฒญ
Controller โ†’ Agent POST /api/v1/nodes/{nodeId}/publish publish ์š”์ฒญ ์ค‘๊ณ„
Controller โ†’ Agent POST /api/v1/nodes/{nodeId}/mesh-freeze ready Peer์—์„œ ํ—ˆ์šฉํ•œ GossipSub ๋ฉ”์‹œ ๋™๊ฒฐ์„ ์ ์šฉํ•˜๊ณ  ์‘๋‹ต ๋ฐ˜ํ™˜
Peer โ†’ Agent POST /api/v1/nodes/{nodeId}/status ํ•ด๋‹น Peer์˜ ์ตœ์‹  ์ƒํƒœ ๋ณด๊ณ 
Peer โ†’ Agent POST /api/v1/telemetry telemetry batch ์ œ์ถœ
Agent โ†’ Peer GET / POST /health / /publish Peer HTTP API๋กœ readiness ํ™•์ธ ๋˜๋Š” publish
Agent โ†’ Peer POST /mesh-freeze ํ•ด๋‹น nodeยทRunยทgeneration์— ๋ฉ”์‹œ ๋™๊ฒฐ ์ ์šฉ

CreateNodeRequest.experimentStartedAt์€ ํ˜„์žฌ ์‹คํ–‰์˜ startedAt์„ ๋ณต์‚ฌํ•œ RFC3339 ์‹œ๊ฐ์ด๋ฉฐ Peer ํ”„๋กœ์„ธ์Šค ์„ค์ •์—๋„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. config.network.schedule์— ๋ณ€๊ฒฝ ๋ชฉ๋ก์ด ์žˆ๊ณ  reference: experiment-start์ด๋ฉด ํ•„์ˆ˜์ด๋ฉฐ, ๋ˆ„๋ฝ ์‹œ Peer๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์ „์— ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. Controller๊ฐ€ ์ž๋™์œผ๋กœ ์ฑ„์šฐ๋Š” ๋‚ด๋ถ€ ์š”์ฒญ ํ•„๋“œ๋กœ ์‹œ๋‚˜๋ฆฌ์˜ค YAML์—๋Š” ๋„ฃ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ •์  network ์„ค์ •๊ณผ Peer ๊ธฐ์ค€ ์˜ˆ์•ฝ์—๋Š” ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

createReplay: true๋ฅผ ์•Œ๋ฆฌ๋Š” Agent๋Š” POST /api/v1/nodes ์‘๋‹ต์ด ๋Š๊ธด ๊ฒฝ์šฐ์˜ ๋ณต๊ตฌ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. Controller๋Š” ํ•ด๋‹น Agent์˜ startedAt์„ CreateNodeRequest.agentStartedAt์— ๋ณต์‚ฌํ•˜๋ฉฐ, Agent ์‹คํ–‰ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋‹ค๋ฅด๋ฉด ์ž…์žฅ ์ „์— 409๋กœ ๊ฑฐ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๊ฐ™์€ ๋…ธ๋“œ ID์™€ ๋™์ผํ•˜๊ฒŒ ๋””์ฝ”๋”ฉ๋œ ์š”์ฒญ์ด๋ฉด ๋™์‹œ ์š”์ฒญ์€ ์ตœ์ดˆ ์ž…์žฅ ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๊ณ , ์ดํ›„ ์š”์ฒญ์€ ๋กœ์ปฌ ์ด๋ ฅ์˜ ์ข…๋ฃŒ ์ƒํƒœ๋ฅผ ํฌํ•จํ•œ ๊ธฐ์กด Peer ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญ ๋‚ด์šฉ์ด ๋‹ค๋ฅด๊ฑฐ๋‚˜ ์ตœ์ดˆ ์ž…์žฅ ์ฒ˜๋ฆฌ๊ฐ€ ์‹คํŒจํ–ˆ๋‹ค๋ฉด 409์ž…๋‹ˆ๋‹ค. Agent๋Š” ๊ธฐ์กด ํ”„๋กœ์„ธ์Šคยท์ด๋ ฅ ๊ธฐ๋ก์˜ metadata.createRequestHash์™€ metadata.createAdmission์— SHA-256 ์š”์ฒญ ์ง€๋ฌธ๊ณผ ์ž…์žฅ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ด€ํ•˜๋ฉฐ ๋ณ„๋„๋กœ ๊ณ„์† ์ปค์ง€๋Š” ์š”์ฒญ ์บ์‹œ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‘ ํ•„๋“œ๋Š” Agent๊ฐ€ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ณต๊ตฌ๋กœ Peer๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜ lifetime์„ ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. agentStartedAt์„ ์ƒ๋žตํ•œ ์š”์ฒญ์€ ๊ธฐ์กด์ฒ˜๋Ÿผ ์ค‘๋ณต ID๋ฅผ ๊ฑฐ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ง€์› Agent์—์„œ๋Š” EOFยท์‘๋‹ต ์ž˜๋ฆผยท์—ฐ๊ฒฐ reset/refusalยทbroken pipeยท์š”์ฒญ timeout์— ํ•œํ•ด ๊ฐ™์€ Agent URLยท์‹คํ–‰ ์ธ์Šคํ„ด์Šค๋กœ ์ด 3ํšŒ๊นŒ์ง€ ์‹œ๋„ํ•˜๋ฉฐ, ์‚ฌ์ด ๋Œ€๊ธฐ๋Š” 250ms์™€ 500ms์ž…๋‹ˆ๋‹ค. ๊ฐ ์š”์ฒญ์˜ ๊ธฐ์กด HTTP ์ œํ•œ์€ 10์ดˆ์ด๊ณ  ์‹œ๋‚˜๋ฆฌ์˜ค context๊ฐ€ ๋” ์งง์œผ๋ฉด ์ด๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ๋ณต๊ตฌ ์ค‘ ์ตœ์ข… ํ”„๋กœํ•„ยทseedยทlifetimeยท๋…ธ๋“œ ID๋Š” ๊ณ ์ •ํ•˜๋ฉฐ ์‹คํ—˜ ์‹œ๊ณ„๋Š” ๊ณ„์† ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. HTTP ๊ฑฐ์ ˆ๊ณผ ์ž˜๋ชป๋œ ํ™•์ธ ์‘๋‹ต์€ ํ†ต์‹  ์žฌ์‹œ๋„ ๋Œ€์ƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. ๊ตฌ๋ฒ„์ „ Agent์—๋Š” ์ƒˆ ํ•„๋“œ๋ฅผ ๋ณด๋‚ด๊ฑฐ๋‚˜ ์ƒ์„ฑ ์š”์ฒญ์„ ์ž๋™ ์žฌ์ „์†กํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ „์†กํ•œ ์š”์ฒญ์˜ ์ƒ์„ฑ ์—ฌ๋ถ€๋ฅผ ๋๋‚ด ํ™•์ธํ•˜์ง€ ๋ชปํ•˜๋ฉด Agent๊ฐ€ ํ•ด๋‹น Peer๋ฅผ ๋ณด๊ณ ํ•˜๊ฑฐ๋‚˜ ํ•ด๋‹น ์‹คํ–‰ ์„ธ๋Œ€์˜ ์ฐจ๋‹จยท์ œ๊ฑฐ ์™„๋ฃŒ๋ฅผ ํ™•์ธํ•  ๋•Œ๊นŒ์ง€ ์˜ˆ์•ฝ ์šฉ๋Ÿ‰์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋ถˆํ™•์‹คํ•œ ์š”์ฒญ์„ ๋‹ค๋ฅธ Agent์— ์žฌ๋ฐฐ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ๋นˆ inventory๋งŒ์œผ๋กœ ๋Šฆ์€ ์ƒ์„ฑ ์š”์ฒญ์ด ๋„์ฐฉํ•˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ๊ณ  ํŒ๋‹จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์˜ˆ์•ฝ Peer์˜ node metadata.networkRequested๋Š” ์š”์ฒญํ•œ network ์„ค์ •์„ ์œ ์ง€ํ•˜๊ณ , metadata.networkSchedule์€ seed๋กœ ํ™•์ •ํ•œ ๊ตฌ์ฒด์  delay๋ฅผ ํฌํ•จํ•œ Agent์˜ ์˜ˆ์•ฝ ๊ณ„ํš์„ ๋‹ด์Šต๋‹ˆ๋‹ค. ์ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ’์€ JSON ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. metadata.network๋Š” ํ˜„์žฌ ๊ตฌ์ฒด์ ์ธ ์กฐ๊ฑด์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ networkPending์ด "true"์ธ ๋™์•ˆ์€ ์•„์ง ์ ์šฉ ํ™•์ธ ์ „์ž…๋‹ˆ๋‹ค. Peer์—์„œ ์ ์šฉ์ด ์„ฑ๊ณตํ•˜๋ฉด ์ƒํƒœ ๋ณด๊ณ ์— networkPending: "false", networkRevision(10์ง„์ˆ˜ ๋ฌธ์ž์—ด, ์ดˆ๊ธฐ ์ƒํƒœ๋Š” 0, ์ดํ›„ ๋ณ€๊ฒฝ ๋ชฉ๋ก์˜ 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ๋ฒˆํ˜ธ), networkAppliedAt(RFC3339)์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ๋Šฆ์€ ์ž…์žฅ์ด๋‚˜ ํƒ€์ด๋จธ ์ง€์—ฐ ์‹œ ์ค‘๊ฐ„ revision์„ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Agent๋Š” ์ด์ „ revision์œผ๋กœ์˜ ์—ญ์ „์„ ๋ง‰๊ณ  heartbeat๋กœ ์ ์šฉ ์ƒํƒœ๋ฅผ Controller์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•„๋“œ๋Š” ๋ณด๊ณ  ์ƒํƒœ์ด๋ฉฐ ์‹คํ–‰ ์ค‘์ธ Peer์˜ ์กฐ๊ฑด์„ ๋ฐ”๊พธ๋Š” API๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.

์ฃผ๊ธฐ์ ์ธ Agent heartbeat๋Š” partial: true๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  JSON ๋ณธ๋ฌธ์„ ์ตœ๋Œ€ 10MiB๋กœ ๋‚˜๋ˆ•๋‹ˆ๋‹ค. ๊ฐœ๋ณ„ Peer ๊ธฐ๋ก์ด ํ•œ๋„๋ฅผ ๋„˜์œผ๋ฉด ๋…ธ๋“œ ์ด๋ฆ„์„ ํฌํ•จํ•œ ์˜ค๋ฅ˜๋ฅผ ๊ธฐ๋กํ•˜๊ณ  ๋ฏธํ™•์ธ ์ƒํƒœ๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์‹คํ–‰ ์ค‘ Peer, ์ปจํ…Œ์ด๋„ˆ ์ œ๊ฑฐ์— ์‹คํŒจํ•œ Peer, Controller ํ™•์ธ์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ข…๋ฃŒ Peer๋Š” ๋ฉ”๋ชจ๋ฆฌ์— ๋‚จ์Šต๋‹ˆ๋‹ค. ์ œ๊ฑฐ๊ฐ€ ํ™•์ธ๋œ ๊ธฐ๋ก์—๋Š” metadata.cleanupComplete="true"๊ฐ€ ๋ถ™์œผ๋ฉฐ, ํ”„๋กœ์„ธ์Šค๊ฐ€ ์‹คํŒจํ–ˆ๋”๋ผ๋„ ์ปจํ…Œ์ด๋„ˆ ์ œ๊ฑฐ๊ฐ€ ์„ฑ๊ณตํ•˜๋ฉด ํ•ด๋‹น๋ฉ๋‹ˆ๋‹ค. ์ตœ์ข… ์ƒํƒœยท์˜ค๋ฅ˜๋Š” ๋ณด์กดํ•˜๋ฉด์„œ ๊ณผ๊ฑฐ ํ† ํด๋กœ์ง€์™€ ์ ์ˆ˜ ๋ฐ์ดํ„ฐ๋Š” ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค.

Controller ํ™•์ธ ์ดํ›„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์€ ์ตœ๊ทผ ์ข…๋ฃŒ ๊ธฐ๋ก 64๊ฐœ๋งŒ ๋‚จ๊ธฐ๊ณ  ์˜ค๋ž˜๋œ ์ถ•์•ฝ ๊ธฐ๋ก์„ ๋กœ์ปฌ bbolt ํŒŒ์ผ ํ•˜๋‚˜์ธ <agent-data-dir>/peer-history/nodes์— ์ œํ•œ๋œ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์ €์žฅํ•œ ํ›„ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ์ฃผ๊ธฐ heartbeat๋Š” ํ™•์ธ๋œ ์ข…๋ฃŒ ๊ธฐ๋ก์„ ์ƒ๋žตํ•ฉ๋‹ˆ๋‹ค. GET /api/v1/status์™€ GET /api/v1/nodes๋Š” ํ˜„์žฌ inventory์™€ ์ตœ๊ทผ/๋ฏธํ™•์ธ ๊ธฐ๋ก์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ๊ณผ๊ฑฐ ๋ชจ๋“  Peer๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. full status๋Š” ๋ˆ„๋ฝ๋œ ํ™œ์„ฑ ๋…ธ๋“œ๋ฅผ ๊ณ„์† ์ •๋ฆฌํ•˜์ง€๋งŒ ์ œ๊ฑฐ๊ฐ€ ํ™•์ธ๋œ ์‹คํŒจ ๊ธฐ๋ก์„ ์„ฑ๊ณต ์ƒํƒœ๋กœ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ณผ๊ฑฐ ๊ธฐ๋ก์€ Controller ๊ฒฐ๊ณผ์™€ Agent ๋กœ์ปฌ ์ด๋ ฅ์— ๋‚จ์Šต๋‹ˆ๋‹ค.

์žฌ๋“ฑ๋กํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ์˜ ์ตœ๊ทผ ๊ธฐ๋ก์„ ๋‹ค์‹œ ์ „๋‹ฌํ•˜๊ณ , ๋งค ํ™œ์„ฑ heartbeat ๋’ค์— ๋””์Šคํฌ ์ด๋ ฅ์„ ์ œํ•œ๋œ ํŽ˜์ด์ง€๋กœ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋Š” ์ตœ๋Œ€ 128๊ฐœยทํ†ต์ƒ 1MiB์ด๋ฉฐ ํฐ ๋‹จ์ผ ๊ธฐ๋ก์€ ๊ธฐ์กด heartbeat ์ƒํ•œ๊นŒ์ง€ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ์‹คํŒจํ•œ ํŽ˜์ด์ง€๋Š” ์žฌ์‹œ๋„ํ•˜๊ณ  ์ „์ฒด ์ด๋ ฅ์„ ํ”„๋กœ์„ธ์Šค ๋งต์— ๋‹ค์‹œ ์ ์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ผ์‹œ์ ์ธ ํ†ต์‹  ์‹คํŒจ๋งŒ์œผ๋กœ ์ „์ฒด ์žฌ์ „์†ก์„ ์‹œ์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ณต์‚ฌํ•œ DB ํ‚ค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์žฌ์ „์†ก์„ ์ด์–ด๊ฐ€๋ฉฐ ๋„คํŠธ์›Œํฌ ์ „์†ก ์ค‘ ์ฝ๊ธฐ ํŠธ๋žœ์žญ์…˜์„ ์œ ์ง€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ œ๊ฑฐ๋œ ๋…ธ๋“œ ID๋Š” ๋””์Šคํฌ ์ธ๋ฑ์Šค ์กฐํšŒ๋กœ ๊ณ„์† ์ค‘๋ณต ์ƒ์„ฑ์„ ๊ฑฐ๋ถ€ํ•˜๋ฉฐ Agent ์žฌ์‹œ์ž‘ ๋’ค์—๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด JSON ์ด๋ ฅ์€ ์‹œ์ž‘ ์‹œ ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ ์ „์— ์ž๋™ ๋ณ€ํ™˜ํ•˜๊ณ  ๋Œ€์กฐ๊ฐ€ ๋๋‚œ ๋ฐฑ์—… ํŒŒ์ผ์„ ์ ์ง„์ ์œผ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ณ€ํ™˜๋œ ์ด๋ ฅ์—๋Š” ํ˜ธํ™˜๋˜๋Š” Agent ๋ฒ„์ „์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ฅ ์ด์ „ ์„ค๋ช…์„ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค. ์‹คํ–‰ ์„ธ๋Œ€ ์ฐจ๋‹จ ์ •๋ณด๋Š” ๋ฉ”๋ชจ๋ฆฌ์— 128๊ฐœ๋ฅผ ์บ์‹œํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” peer-history/fences/์— ๋ณด๊ด€ํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ž˜๋œ stop ์š”์ฒญ์ด ์ €์žฅ๋œ ์ฐจ๋‹จ ์„ธ๋Œ€๋ฅผ ๋‚ฎ์ถœ ์ˆ˜ ์—†์œผ๋ฉฐ ์ „์ฒด ์ •์ง€๋„ ์ด๋ฏธ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ œ๊ฑฐํ•œ run๊นŒ์ง€ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.

Controller ํ™•์ธยท๋กœ์ปฌ ์ •๋ฆฌ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ข…๋ฃŒ ๊ธฐ๋ก ๋˜๋Š” ์ด๋ฒคํŠธ ํ ์ง„์ž…์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ข…๋ฃŒ ํ‘œ์‹์ด ๊ฐ๊ฐ 4,096๊ฐœ์— ๋„๋‹ฌํ•˜๋ฉด ์ƒˆ Peer ์ƒ์„ฑ์€ Retry-After: 1๊ณผ ํ•จ๊ป˜ 503์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด Peer์™€ ๋ฏธ์ „๋‹ฌ ์ฆ๊ฑฐ๋Š” ์œ ์ง€ํ•˜๊ณ  Docker ์ œ๊ฑฐ ์‹คํŒจ๋Š” ๊ณ„์† ์šฉ๋Ÿ‰์„ ์ ์œ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ฅ ์กฐํšŒยท์ €์žฅ I/O๋Š” Agent ์ƒํƒœ ์ž ๊ธˆ ๋ฐ–์—์„œ ์ˆ˜ํ–‰ํ•˜๋ฉฐ, ์ฐจ๋‹จ/์ด๋ ฅ ๋ณ€๊ฒฝ ๋ฒˆํ˜ธ๋ฅผ ์žฌํ™•์ธํ•˜์—ฌ ๋””์Šคํฌ ์กฐํšŒ์™€ ์ •๋ฆฌ๊ฐ€ ๊ต์ฐจํ•  ๋•Œ ์˜ค๋ž˜๋œ ์ƒ์„ฑ ์š”์ฒญ์ด ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

telemetry ์š”์ฒญ์€ ์ด๋ฒคํŠธ 5000๊ฐœ์™€ JSON ๋ณธ๋ฌธ 10 MiB๋กœ ์ œํ•œ๋ฉ๋‹ˆ๋‹ค. Peer์™€ Agent๋Š” ์ด์Šค์ผ€์ดํ”„์™€ envelope๋ฅผ ํฌํ•จํ•œ ์ธ์ฝ”๋”ฉ ๋ฐ”์ดํŠธ ์ˆ˜๋กœ batch๋ฅผ ๋‚˜๋ˆ„๋ฉฐ, ์žฌ์‹œ๋„์—๋„ ์›๋ณธ ์ˆœ์„œ์™€ ์ด๋ฒคํŠธ ์‹๋ณ„์ž๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜๋ฝ๋œ ์•ž๋ถ€๋ถ„์€ ์ œ๊ฑฐํ•œ ํ›„ ๋‚˜๋จธ์ง€๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ๋‘ ์ˆ˜์‹ ์ž๋Š” admission ์ „์— ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ผ ์ด๋ฒคํŠธ๊ฐ€ ์ธ์ฝ”๋”ฉ๋œ 10 MiB batch์— ๋“ค์–ด๊ฐˆ ์ˆ˜ ์—†์œผ๋ฉด 413์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ํ•ด๋‹น ์š”์ฒญ์˜ ์ด๋ฒคํŠธ๋Š” ํ•˜๋‚˜๋„ ์ˆ˜๋ฝํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Agent๋Š” ์ž์‹ ์˜ ์‹๋ณ„์ž๋กœ ์ •๊ทœํ™”ํ•œ ๋’ค ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. Controller์— ์ง์ ‘ ์ œ์ถœํ•œ ์ด๋ฒคํŠธ๋„ ์ด ๊ฒ€์‚ฌ๋ฅผ ๊ฑฐ์น˜๋ฏ€๋กœ ์ €์žฅ ๋ถ„์„์—์„œ ์ฝ์„ ์ˆ˜ ์—†๋Š” ๊ณผ๋Œ€ ๋กœ๊ทธ ํ–‰์„ ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Controller๋Š” ๋””์ฝ”๋”ฉ๋ถ€ํ„ฐ ์˜๊ตฌ ์ €์žฅ๊นŒ์ง€ telemetry ์š”์ฒญ์„ ์ตœ๋Œ€ ๋‘ ๊ฐœ ์ˆ˜๋ฝํ•ฉ๋‹ˆ๋‹ค. ์ถ”๊ฐ€ ์š”์ฒญ์€ ์ตœ๋Œ€ 64๊ฐœ๊นŒ์ง€ ๋ณธ๋ฌธ์„ ์ฝ์ง€ ์•Š์€ ์ฑ„ ์ตœ๋Œ€ 1์ดˆ ๋Œ€๊ธฐํ•˜๋ฉฐ, ์ทจ์†Œ๋˜๋ฉด ๋Œ€๊ธฐ๋ฅผ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. ํฌํ™” ์‹œ 503, Retry-After: 1์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์ˆ˜์‹  ํ™•์ธ์„ ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ Agent๋Š” ํ•ด๋‹น batch๋ฅผ ๋ณด๊ด€ํ•˜๊ณ  ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅ์„ ์‹œ์ž‘ํ•œ ์š”์ฒญ์€ ํด๋ผ์ด์–ธํŠธ ์‹œ๊ฐ„ ์ดˆ๊ณผ ํ›„ ์™„๋ฃŒ๋  ์ˆ˜ ์žˆ์ง€๋งŒ ์ด๋ฒคํŠธ ์‹๋ณ„์ž๋กœ ์žฌ์‹œ๋„์˜ ์ด์ค‘ ์ง‘๊ณ„๋ฅผ ๋ง‰์Šต๋‹ˆ๋‹ค. ์ด ์ œํ•œ์€ ๋™์‹œ์— ๋””์ฝ”๋”ฉํ•œ ๋ณธ๋ฌธ์˜ ์ˆ˜๋ฅผ ์ œํ•œํ•˜๋ฉฐ ์ „์ฒด Go heap ์ƒํ•œ์€ ์•„๋‹™๋‹ˆ๋‹ค.

Agent๋Š” ์ˆ˜์‹  ํ™•์ธ ์ „์— <agent-data-dir>/telemetry-spool/์— telemetry batch๋ฅผ ์˜๊ตฌ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ํ™•์ธ๋œ ์•ž๋ถ€๋ถ„์˜ cursor๋ฅผ ์ €์žฅํ•˜์—ฌ ์žฌ์‹œ์ž‘ ์‹œ ๋‚˜๋จธ์ง€๋ฅผ ์žฌ์ „์†กํ•˜๊ณ  Controller๋„ ๋กœ์ปฌ ์ด๋ฒคํŠธ batch๋ฅผ ๋””์Šคํฌ์— ๋™๊ธฐํ™”ํ•œ ๋’ค ์ˆ˜์‹  ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. Agent ๊ต์ฒด ์‹œ ๋ฐ์ดํ„ฐ volume์„ ์œ ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ตœ์†Œ ํ•œ ๋ฒˆ ์ „๋‹ฌ ๋ฐฉ์‹์ด๋ฏ€๋กœ ์žฌ์ „์†ก ๋•Œ ์ด๋ฒคํŠธ ID๋ฅผ ์œ ์ง€ํ•˜๋ฉฐ, Peer์—์„œ ์ˆ˜๋ฝํ•˜์ง€ ๋ชปํ•œ ์ด๋ฒคํŠธ๋‚˜ ์žƒ์–ด๋ฒ„๋ฆฐ volume์˜ ์ž๋ฃŒ๋Š” ๋ณต์›ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

Spool ๋ณต๊ตฌ๋Š” ํŒŒ์ผ ์œ„์น˜/Run ์ธ๋ฑ์Šค์™€ ์ตœ๋Œ€ 5,000๊ฐœ / ์ง๋ ฌํ™” ๊ธฐ์ค€ 1 MiB ์ „์†ก ๊ตฌ๊ฐ„๋งŒ ๋ฉ”๋ชจ๋ฆฌ์— ๋‘๋ฉฐ, ๊ฐœ๋ณ„์ ์œผ๋กœ ์œ ํšจํ•œ ํฐ ์ด๋ฒคํŠธ ํ•˜๋‚˜๋Š” ๊ตฌ๊ฐ„ ํฌ๊ธฐ๋ฅผ ๋„˜์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. telemetry ๋””์ฝ”๋”ฉยท์ €์žฅ์€ ๋™์‹œ 2๊ฐœ๋กœ ์ œํ•œํ•˜๊ณ , ์ถ”๊ฐ€ 64๊ฐœ ์š”์ฒญ๊นŒ์ง€๋งŒ ๋ณธ๋ฌธ์„ ์ฝ์ง€ ์•Š๊ณ  ์ตœ๋Œ€ 1์ดˆ ๋Œ€๊ธฐํ•ฉ๋‹ˆ๋‹ค. ๋””์ฝ”๋” ํฌํ™” ๋˜๋Š” ์ƒˆ batch๊ฐ€ ๋Œ€๊ธฐ๋Ÿ‰ 50,000๊ฐœ / 256 MiB๋ฅผ ๋„˜๊ธฐ๋ฉด 503๊ณผ Retry-After๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์ด๋ฏธ ์ˆ˜๋ฝํ•œ ๋””์Šคํฌยท์ „์†ก ์ค‘ ์ž๋ฃŒ๋Š” ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ๋Œ€๊ธฐ๋Ÿ‰์ด ๊ฐ€๋“ ์ฐฌ ๋™์•ˆ ์ƒˆ Peer ๋ฐฐ์น˜๋„ ์ผ์‹œ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ์‹คํ–‰ Peer์˜ ์ตœ์ข… ์ฆ๊ฑฐ๋Š” ์ผ์‹œ์ ์œผ๋กœ ํ•œ๋„๋ฅผ ๋„˜์„ ์ˆ˜ ์žˆ๊ณ  ๊ณผ๊ฑฐ์˜ ๋” ํฐ ์ ์ฒด๋„ ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ  ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. Run drain์€ ์•„์ง ๋””์Šคํฌ์— ๋‚จ์€ ํ•ด๋‹น Run ์ด๋ฒคํŠธ๊นŒ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

POST /api/v1/nodes์˜ ์ž…์žฅ ์ „ ๊ฑฐ์ ˆ์—๋Š” X-KPL-Admission-Retry: not-created์™€ Retry-After๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค. Controller๋Š” ์˜ˆ์•ฝ์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ํ•ด๋‹น Agent๋ฅผ ์ž ์‹œ ์ œ์™ธํ•œ ๋’ค ์‹œ๋‚˜๋ฆฌ์˜ค context ์•ˆ์—์„œ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ๋ฏธ์ง€์ • ๋ฐฐ์น˜๋Š” ๋‹ค๋ฅธ Agent๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ์ง€์ •/single-agent ๋ฐฐ์น˜๋Š” ๋Œ€์ƒ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ˆœ์ฐจ ์—…๋ฐ์ดํŠธ ์ค‘์—๋Š” ๊ธฐ์กด telemetryยท์ข…๋ฃŒ ์ฆ๊ฑฐ ์ ์ฒด ์˜ค๋ฅ˜ ๋‘ ์ข…๋ฅ˜๋„ Retry-After๊ฐ€ ์žˆ์„ ๋•Œ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜ 503/timeout์€ ๋ฌด์กฐ๊ฑด ์žฌ๋ฐฐ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ƒˆ spool์€ 4 MiB ๋ชฉํ‘œ .jsonl ํŒŒ์ผ์— batch๋ฅผ ๋ชจ์œผ๊ณ  ์ฒดํฌ์„ฌ ACK journal์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ๊ธฐ์กด .json๋„ ๋ณต๊ตฌํ•ฉ๋‹ˆ๋‹ค. format-v2.json ํ‘œ์‹์œผ๋กœ ๊ตฌ๋ฒ„์ „ Agent๊ฐ€ ์ƒˆ ํ˜•์‹์˜ ๋ฏธ์ „๋‹ฌ ์ž๋ฃŒ๋ฅผ ๋ฌด์‹œํ•˜์ง€ ๋ชปํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ์ „๋‹ฌ์„ ๋งˆ์นœ ์ •์ƒ ์ข…๋ฃŒ์—์„œ ํ‘œ์‹๊ณผ ๋นˆ ํ™œ์„ฑ ํŒŒ์ผ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

Agent ์ •์ƒ ์ข…๋ฃŒ ๋˜๋Š” DELETE /api/v1/nodes์˜ telemetry drain์€ ๋งˆ์ง€๋ง‰ Peer ์ด๋ฒคํŠธ๋ฅผ ํฌํ•จํ•ด ๋Œ€๊ธฐ ํ์™€ ์ด๋ฏธ ์ „์†ก ์ค‘์ธ batch ๋ชจ๋‘ Controller์˜ ์ˆ˜์‹  ํ™•์ธ์„ ๋ฐ›์„ ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์‹คํŒจํ•œ batch๋Š” ์ˆœ์„œ๋ฅผ ์œ ์ง€ํ•ด ์žฌ์‹œ๋„ํ•˜๋ฉฐ, ์ œํ•œ ์‹œ๊ฐ„ ๋‚ด ์ˆ˜์‹  ํ™•์ธ์„ ์™„๋ฃŒํ•˜์ง€ ๋ชปํ•˜๋ฉด drain ์‹คํŒจ๋ฅผ ๋ณด๊ณ ํ•ฉ๋‹ˆ๋‹ค.

JSON ๋ณธ๋ฌธ์—๋Š” ๊ฐ’ ํ•˜๋‚˜๋งŒ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ณธ๋ฌธ ํ•œ๋„ ๋‚ด์˜ ํ›„ํ–‰ ๊ณต๋ฐฑ์€ ํ—ˆ์šฉํ•˜๋ฉฐ, ๋‘ ๋ฒˆ์งธ JSON ๊ฐ’ยทํ›„ํ–‰ ์“ฐ๋ ˆ๊ธฐ ๋ฐ์ดํ„ฐยท๋””์ฝ”๋” ๋ณธ๋ฌธ ํ•œ๋„ ์ดˆ๊ณผ๋Š” 400์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Controller/Agent์˜ ์ผ๋ฐ˜ JSON handler๋Š” 10 MiB, Peer /publish๋Š” 1 MiB, /mesh-freeze๋Š” 4 KiB ํ•œ๋„๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์‹œ๋‚˜๋ฆฌ์˜ค ์š”์ฒญ envelope์—๋Š” ์œ„์—์„œ ์„ค๋ช…ํ•œ ๋ณ„๋„ ํ•œ๋„๊ฐ€ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. Peer ๋‚ด๋ถ€์—์„œ ์ƒ์„ฑ๋œ ์ด๋ฒคํŠธ๊ฐ€ JSON์œผ๋กœ ์ธ์ฝ”๋”ฉ๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋‹จ์ผ batch ํ•œ๋„๋ฅผ ๋„˜์œผ๋ฉด ๋กœ๊ทธ์™€ telemetry_drop์— ์œ ์‹ค ์ˆ˜๋ฅผ ๋‚จ๊ธฐ๊ณ , ์†Œ์Šค sequence์˜ ๋นˆ ๋ฒˆํ˜ธ๋ฅผ ์œ ์ง€ํ•œ ์ฑ„ ํ›„์† ์ด๋ฒคํŠธ๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์‹คํŒจ ์‹œ์—๋Š” ๋Œ€๊ธฐ ์ค‘์ธ batch๋ฅผ ๋ณด์กดํ•ด ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.

Peer /publish์˜ 10์ดˆ ๋ฐœํ–‰ ์ œํ•œ์‹œ๊ฐ„์—๋Š” ๋‹ค๋ฅธ ๋กœ์ปฌ ๋ฐœํ–‰ ๋’ค์—์„œ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์‹œ๊ฐ„๋„ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์š”์ฒญ ์ทจ์†Œ๋‚˜ ์ œํ•œ์‹œ๊ฐ„ ๋งŒ๋ฃŒ ์‹œ ํ˜„์žฌ ๋ฐœํ–‰์˜ ์ข…๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋Œ€๊ธฐ์—์„œ ๋น ์ ธ๋‚˜์˜ค๋ฉฐ, gate/readiness ๋Œ€๊ธฐ ์ค‘์—๋Š” 504๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€๊ธฐ ์ค‘ ์ทจ์†Œ๋œ ์š”์ฒญ์€ ๋ฐœํ–‰ํ•˜๊ฑฐ๋‚˜ ์„ฑ๊ณต publish ์ด๋ฒคํŠธ๋ฅผ ๋‚จ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐœํ–‰ ์‹œ๊ฐ์€ ๊ธฐ์กด์ฒ˜๋Ÿผ gate์™€ readiness ๋Œ€๊ธฐ ์ดํ›„์— ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.

Agent๋Š” Controller๊ฐ€ cleanup์— ์‚ฌ์šฉํ•˜๋Š” ๋‹ค์Œ endpoint๋„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Method Agent path ์„ค๋ช…
POST /api/v1/runs/{runId}/drain ํ•ด๋‹น Run์˜ ๋Œ€๊ธฐยท์ „์†ก ์ค‘ telemetry๊ฐ€ Controller์— ์ˆ˜์‹  ํ™•์ธ๋  ๋•Œ๊นŒ์ง€ ์ตœ๋Œ€ 30์ดˆ ๋Œ€๊ธฐ. ์„ฑ๊ณต 204, ์‹คํŒจ 503. ๋‹ค๋ฅธ Run์˜ ์ „์†ก์€ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์Œ.
DELETE /api/v1/runs/{runId}/nodes?generation=N unsigned generation์ด ํ•„์ˆ˜์ด๋ฉฐ, run fence๋ฅผ N๊นŒ์ง€ ์›์ž์ ์œผ๋กœ ๋†’์ด๊ณ  ์ดํ›„ generation N ์ดํ•˜์˜ create๋ฅผ ๊ฑฐ๋ถ€ํ•˜๋ฉฐ ํ•ด๋‹น generation์˜ ๊ธฐ์กด ๋…ธ๋“œ๋ฅผ ์ข…๋ฃŒํ•œ ๋’ค 202 Accepted๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๋ถ€ endpoint๋Š” ์šด์˜์ž API์™€ ๋ณ„๊ฐœ๋กœ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์š”์ฒญ๊ณผ snapshot์˜ ํ•„๋“œ ์ •์˜๋Š” internal/model/model.go, ๋Œ€์—ญํญ ํƒ€์ž…์€ internal/model/bandwidth.go, handler๋Š” internal/controller/api.go์™€ internal/agent/api.go์— ์žˆ์Šต๋‹ˆ๋‹ค.

Scoring๊ณผ inspection์ด ํ™œ์„ฑํ™”๋˜๋ฉด Peer ์ƒํƒœ์— scoreSample: {observedAt, sequence, intervalSeconds, components}๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ํ•ญ๋ชฉ์€ ํ•ด๋‹น ๊ด€์ธก Peer๊ฐ€ ํ‰๊ฐ€ํ•œ ์œ ํšจํ•œ ์ƒ๋Œ€ Peer ํ‘œ๋ณธ์˜ {count, mean, min, max}์ž…๋‹ˆ๋‹ค. ํ‚ค๋Š” p1, p2, p3, p3b, p4, p5, p6, p7, topicCap, total์ž…๋‹ˆ๋‹ค. Agent๋Š” snapshot์„ ๋ณต์‚ฌํ•ด ๋ณด์กดํ•˜๊ณ  ์˜ค๋ž˜๋œ ๊ตฌ์„ฑ ํ•ญ๋ชฉ ๋ณด๊ณ ๋ฅผ ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒด node/network/snapshot REST์—๋Š” ์ œ๊ณตํ•˜์ง€๋งŒ Dashboard SSE์—์„œ๋Š” ๊ตฌ์„ฑ ํ•ญ๋ชฉ ์š”์•ฝ์„ ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. peer_score ์ด๋ฒคํŠธ๋Š” ๊ฐ™์€ scoreSample์„ ์†Œ์Šค ์‹๋ณ„์žยท์ด๋ฒคํŠธ sequence์™€ ํ•จ๊ป˜ events.jsonl์— ์ €์žฅํ•˜๋ฉฐ ์ตœ๊ทผ ๋„คํŠธ์›Œํฌ ์ด๋ฒคํŠธ ๋ชฉ๋ก์„ ์ฑ„์šฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด ์ƒ๋Œ€ Peer๋ณ„ peerScores ์ด์ ๋„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ’์€ ์š”์•ฝ ํ†ต๊ณ„์ด๋ฉฐ ๊ฐœ๋ณ„ ์ƒ๋Œ€ Peerยทtopic ์Œ์˜ ์ „์ฒด ํ–‰๋ ฌ์„ ์ €์žฅํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค.

์„ ํƒ์  ํ‘œ๋ณธ sequence๋Š” Peer ํ”„๋กœ์„ธ์Šค ์•ˆ์—์„œ ์ฆ๊ฐ€ํ•˜๋Š” ์–‘์ˆ˜ inspection ์ˆœ๋ฒˆ์ด๋ฉฐ ๋ฐ”๊นฅ ์ด๋ฒคํŠธ์˜ sequence์™€ ๋…๋ฆฝ์ ์ž…๋‹ˆ๋‹ค. Agent๋Š” ์ด ๊ฐ’์œผ๋กœ Controller ์‹œ๊ณ„ offset์ด ๋’ค๋กœ ๋ณด์ •๋œ ๋’ค์—๋„ ๋” ์ตœ์‹ ์ธ ๊ด€์ธก๊ณผ ๋นˆ snapshot์„ ๋ฐ›์•„๋“ค์ž…๋‹ˆ๋‹ค. observedAt์€ ๋ณด์ •ํ•œ snapshot ์‹œ๊ฐ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ˆœ๋ฒˆ์ด ์—†๊ฑฐ๋‚˜ 0์ธ ๊ธฐ์กด ํ‘œ๋ณธ์€ ์ˆœ๋ฒˆ ์žˆ๋Š” ํ‘œ๋ณธ์ด ๋„์ฐฉํ•  ๋•Œ๊นŒ์ง€ observedAt์œผ๋กœ ์ •๋ ฌํ•˜๋ฉฐ, ์ˆœ๋ฒˆ ์žˆ๋Š” ํ‘œ๋ณธ์„ ๋ฎ์–ด์“ฐ์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ์ƒˆ Peer ํ”„๋กœ์„ธ์Šค๋Š” Agent๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” ์ƒํƒœ ์ˆ˜๋ช…๋„ ์ƒˆ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

Mesh freeze command

์‹œ๋‚˜๋ฆฌ์˜ค v3์˜ mesh-freeze action์€ ๋‚ด๋ถ€ Bearer ์ธ์ฆ์œผ๋กœ ๋Œ€์ƒ Agent์˜ POST /api/v1/nodes/{nodeId}/mesh-freeze๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. Agent๋Š” ๋‚ด๋ถ€ token๊ณผ X-KPL-Node-ID: {nodeId} ํ—ค๋”๋ฅผ ๋ถ™์—ฌ ๊ฐ™์€ JSON์„ Peer์˜ POST /mesh-freeze๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญ๊ณผ ์„ฑ๊ณต 200 ์‘๋‹ต์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

{"runId":"run-id","generation":1}
{"nodeId":"node-id","peerId":"actual-libp2p-peer-id","frozen":true}

Agent๋Š” ์ „์†ก ์ „์— readiness, GossipSub ํ™œ์„ฑํ™”, meshFreeze ํ—ˆ์šฉ ์„ค์ •, ์ •ํ™•ํ•œ Runยทgeneration๊ณผ run fence๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์‘๋‹ต์˜ node/Peer ID๋ฅผ ํ™•์ธํ•˜๊ณ  ๊ฐ™์€ ํ”„๋กœ์„ธ์Šคยท์‹คํ–‰ ๋ฒ”์œ„๊ฐ€ ์œ ์ง€๋˜๋Š”์ง€ ๋‹ค์‹œ ํ™•์ธํ•œ ๋’ค ์ ์šฉ ์‘๋‹ต์„ ์ˆ˜๋ฝํ•ฉ๋‹ˆ๋‹ค. Peer๋„ node IDยทRunยทgeneration ์ผ์น˜๋ฅผ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค. ์ง€์—ฐ๋œ ์š”์ฒญ์ด๋‚˜ ์žฌ์‚ฌ์šฉ๋œ ์ปจํ…Œ์ด๋„ˆ ์ฃผ์†Œ๋กœ ๋‹ค๋ฅธ Peer๋ฅผ ๋™๊ฒฐํ•˜์ง€ ์•Š๋„๋ก ํ•˜๋Š” ๊ฒ€์‚ฌ์ž…๋‹ˆ๋‹ค. ์„ฑ๊ณต์€ ๋กœ์ปฌ PubSub ์ด๋ฒคํŠธ ๋ฃจํ”„๊ฐ€ ๋‹จ๋ฐฉํ–ฅยท๋ฉฑ๋“ฑ ๋™๊ฒฐ์„ ์ ์šฉํ–ˆ๋‹ค๋Š” ๋œป์ด๋ฉฐ ์—ฌ๋Ÿฌ Peer์˜ ๋™์‹œ ์ ์šฉ์„ ๋ณด์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ณ„์†๋˜๋Š” ํ† ํด๋กœ์ง€ยท๋ฉ”์‹œ์ง€ ๋™์ž‘์€ ํ”„๋กœํ† ์ฝœ ์ •์˜๋ฅผ ์ฐธ๊ณ ํ•˜์‹ญ์‹œ์˜ค.

์‹œ๋‚˜๋ฆฌ์˜ค timeout์€ ์ „์ฒด ์„ ํƒ ๋Œ€์ƒ์˜ ์ „์†ก๊ณผ ์ ์šฉ ์‘๋‹ต์„ ํ•จ๊ป˜ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. ๊ฐ Agentโ†’Peer ์š”์ฒญ๊ณผ Peer handler์˜ ์ œํ•œ์€ ๊ณ„์† 10์ดˆ์ด๋ฉฐ ์‹œ๋‚˜๋ฆฌ์˜ค timeout์„ ๋Š˜๋ ค๋„ ๊ฐœ๋ณ„ ์ œํ•œ์€ ๋Š˜์–ด๋‚˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ทจ์†Œ๋Š” ์ œ์–ด ์š”์ฒญ์„ ๋”ฐ๋ผ ์ „๋‹ฌ๋˜์–ด ์ด๋ฒคํŠธ ๋ฃจํ”„์˜ ๋Œ€๊ธฐ๋ฅผ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค.

Agent๋Š” ์ž˜๋ชป๋œ ์ž…๋ ฅ์ด๋‚˜ ๋ˆ„๋ฝ๋œ runId์— 400, ๋Œ€์ƒ ๊ฒ€์‚ฌยท์ „๋‹ฌยท์ ์šฉ ์‘๋‹ต ํ™•์ธ ์‹คํŒจ์— 502๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Peer๋Š” ์ž˜๋ชป๋˜๊ฑฐ๋‚˜ ์•Œ ์ˆ˜ ์—†๋Š” JSON ํ•„๋“œ์— 400, ์‹๋ณ„์ž ๋˜๋Š” ์„ค์ • ๋ถˆ์ผ์น˜์— 409, ์ข…๋ฃŒ๋‚˜ ์ทจ์†Œ์— 503, 10์ดˆ ์ ์šฉ ์ œํ•œ์‹œ๊ฐ„ ๋งŒ๋ฃŒ์— 504๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋‘ endpoint๋Š” ๋‚ด๋ถ€ ์ธ์ฆ ๋ˆ„๋ฝยท๋ถˆ์ผ์น˜์— 401์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  POST๋ฅผ ์š”๊ตฌํ•˜๋ฉฐ ๋‹ค๋ฅธ method์—๋Š” 405๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ ์šฉ ํ›„ ์‘๋‹ต์ด ์œ ์‹ค๋˜๋ฉด ์š”์ฒญ์ด ์‹คํŒจํ–ˆ์–ด๋„ Peer๋Š” ๋™๊ฒฐ๋œ ์ƒํƒœ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ๋„๋Š” ๋ฉฑ๋“ฑ์ด๋ฉฐ rollback์ด๋‚˜ ๋™๊ฒฐ ํ•ด์ œ๋Š” ์—†์Šต๋‹ˆ๋‹ค.

Node metadata์˜ ๋ฌธ์ž์—ด ๊ฐ’ meshFreezeEnabled๋Š” ์„ค์ •๋œ ํ—ˆ์šฉ ์—ฌ๋ถ€, meshFrozen์€ ์ ์šฉ ํ™•์ธ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. meshFrozen์ด true๊ฐ€ ๋˜๋ฉด ์ดํ›„ ์ƒํƒœ ๋ณด๊ณ ๊ฐ€ ์ด๋ฅผ ๋˜๋Œ๋ฆฌ์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. Peer๋Š” ์ฒ˜์Œ ๋™๊ฒฐ์„ ํ™•์ธํ•  ๋•Œ fields.frozen: true, scope: "peer", membership: "pinned"๋ฅผ ํฌํ•จํ•œ mesh_freeze ์ด๋ฒคํŠธ๋ฅผ ๋‚ด๋ณด๋‚ด๋ฉฐ ๋ฐ˜๋ณต ์„ฑ๊ณต ์‹œ ์ด๋ฒคํŠธ๋ฅผ ์ค‘๋ณต ์ƒ์„ฑํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜ telemetry์˜ ์ „๋‹ฌยท์œ ์‹ค ๊ฒฝ๊ณ„๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์š”์ฒญยท์‘๋‹ต ํƒ€์ž…์€ internal/model/mesh_freeze.go์— ์žˆ์Šต๋‹ˆ๋‹ค.

Topology command

์‹œ๋‚˜๋ฆฌ์˜ค v3์˜ topology๋Š” Agent์˜ POST /api/v1/nodes/{nodeId}/topology๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  Agent๊ฐ€ Peer์˜ POST /topology๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋‚ด๋ถ€ Bearer ์ธ์ฆ, X-KPL-Node-ID, ์ •ํ™•ํ•œ Runยทgeneration, readiness์™€ run fence ๊ฒ€์‚ฌ๋Š” mesh-freeze์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋‘ endpoint๋Š” JSON ๊ฐ’ ํ•˜๋‚˜๋งŒ ํ—ˆ์šฉํ•˜๊ณ  ์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ๋ฅผ ๊ฑฐ๋ถ€ํ•˜๋ฉฐ ์š”์ฒญ๊ณผ ์‘๋‹ต์„ 1 MiB๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.

{
  "runId": "run-id",
  "generation": 0,
  "topologyId": "controller-generated-sha256",
  "stage": "prepare",
  "topic": "kpl/graph",
  "neighbors": [{"peerId": "actual-libp2p-peer-id", "addresses": ["/ip4/10.0.0.2/tcp/20000"]}]
}

prepare๋Š” ์ „์ฒด ์ž…๋ ฅ์„ ๊ฒ€์‚ฌํ•˜๊ณ  ํ•„์š”ํ•œ ์—ฐ๊ฒฐ๊ณผ PubSub ๊ตฌ๋…์„ ํ™•์ธํ•œ ๋’ค ์ค€๋น„๋œ ๊ณ„ํš ํ•˜๋‚˜๋ฅผ ๋ณด๊ด€ํ•˜๋ฉฐ ๋ฉ”์‹œ๋ฅผ ์„ค์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๋Œ€์ƒ์ด ์ค€๋น„๋˜๋ฉด ๊ฐ™์€ ๊ณ„ํš IDยทํ† ํ”ฝยท์ด์›ƒ ๋ชฉ๋ก์˜ apply๊ฐ€ ์ •ํ™•ํ•œ ํ† ํ”ฝ ๋ฉ”์‹œ๋ฅผ ์„ค์น˜ํ•˜๊ณ  ๋ชจ๋“  ๋กœ์ปฌ ํ† ํ”ฝ ๋ฉ”์‹œ๋ฅผ ๋™๊ฒฐํ•ฉ๋‹ˆ๋‹ค. topologyId๋Š” ์ „์ฒด ๊ทธ๋ž˜ํ”„์™€ Peer ์‹คํ–‰ ์„ธ๋Œ€๋ฅผ ์‹๋ณ„ํ•˜๋ฉฐ ๋„๋ฉ”์ธ ์ „์ฒด๊ฐ€ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ์ž๊ธฐ ์ž์‹ ยท์ค‘๋ณต ์ด์›ƒ๊ณผ ์ž˜๋ชป๋˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ Peer๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š” /p2p ์ฃผ์†Œ suffix๋Š” ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. ํ•œ Peer์˜ ์ด์›ƒ์€ ์ตœ๋Œ€ 9,999๊ฐœ, ์ด์›ƒ๋ณ„ ์ฃผ์†Œ๋Š” 1โ€“32๊ฐœ, ์ฃผ์†Œ ๊ธธ์ด๋Š” ์ตœ๋Œ€ 2,048 byte์ž…๋‹ˆ๋‹ค.

์„ฑ๊ณต 200 ์‘๋‹ต์€ {nodeId, peerId, topologyId, stage, topic, frozen, neighbors}์ด๋ฉฐ neighbors๋Š” ์ •ํ™•ํ•œ Peer ID ๋ชฉ๋ก์„ ์ •๋ ฌํ•œ ๊ฐ’์ž…๋‹ˆ๋‹ค. Apply ์‘๋‹ต์€ frozen: true์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. Agent์™€ Controller๋Š” ์‹๋ณ„์žยท์ด์›ƒ ๋ชฉ๋ก์„ ๊ฒ€์‚ฌํ•œ ๋’ค ๊ฐ™์€ Peer ์‹คํ–‰ ์„ธ๋Œ€์ธ์ง€ ๋‹ค์‹œ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. Peer ์ž‘์—… ์ œํ•œ์€ 30์ดˆ์ด๊ณ , ํ•˜์œ„ ๋‹จ๊ณ„์˜ timeout ์‘๋‹ต์„ ๋ฐ›์„ ์—ฌ์œ ๋ฅผ ๋‘์–ด Agentโ†’Peer ์š”์ฒญ์€ 35์ดˆ, Controllerโ†’Agent ์š”์ฒญ์€ 40์ดˆ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค timeout์ด ๋” ์งง์œผ๋ฉด ํ•ด๋‹น ์ œํ•œ์„ ์šฐ์„ ํ•ฉ๋‹ˆ๋‹ค. ์‘๋‹ต ์œ ์‹ค ๋’ค ์ด๋ฏธ ์ ์šฉํ•œ ๋™์ผ ๊ณ„ํš์„ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค์‹œ ์ œ์ถœํ•˜๋Š” ๊ฒƒ์€ ํ—ˆ์šฉํ•˜๋ฉฐ, ๋ฌผ๋ฆฌ ๋งํฌ๊ฐ€ ์‚ฌ๋ผ์กŒ์–ด๋„ ๋ฉค๋ฒ„์‹ญ ์ผ์น˜ ์‹œ ์„ฑ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ ์šฉ ๊ณ„ํš์€ ์ถฉ๋Œ์ž…๋‹ˆ๋‹ค. Prepare ์„ฑ๊ณต์€ ์›๊ฒฉ ์ƒํƒœ๋ฅผ ์˜ˆ์•ฝํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ apply์—์„œ ํ˜„์žฌ ์กฐ๊ฑด์„ ๋‹ค์‹œ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.

Controller๋Š” Peer๋ณ„ ์ค€๋น„ ๋ช…๋ น์„ ์ตœ๋Œ€ ์„ธ ๋ฒˆ(์ตœ์ดˆ ์š”์ฒญ๊ณผ ๋‘ ๋ฒˆ์˜ ์žฌ์‹œ๋„) ๋ณด๋ƒ…๋‹ˆ๋‹ค. ์ผ์‹œ์ ์ธ HTTP 503/504, ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜, ๊ฐœ๋ณ„ ์š”์ฒญ timeout๋งŒ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ ์ „ ๊ฐ๊ฐ 250 ms์™€ 500 ms๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ ์ทจ์†Œ ์‹œ ๋Œ€๊ธฐ๋„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. ๋งค ์‹œ๋„๋งˆ๋‹ค ์„ ํƒํ•œ Peer์˜ ์‹คํ–‰ ์„ธ๋Œ€๋ฅผ ๋‹ค์‹œ ๊ฒ€์‚ฌํ•˜๊ณ  ๊ฐ™์€ ๊ณ„ํš์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋ชจ๋“  ์‹œ๋„๋Š” ๊ฐ™์€ ์ „์ฒด phase ์ œํ•œ์‹œ๊ฐ„์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ฆ ์˜ค๋ฅ˜, Peer 409 ๊ฐ™์€ ์˜๊ตฌ ์ถฉ๋Œ๊ณผ ์ž˜๋ชป๋œ ์ ์šฉ ์‘๋‹ต์€ ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ •ํ™•ํ•œ ๊ณ„ํš์˜ prepare ์‘๋‹ต์„ ๋ชจ๋“  ๋Œ€์ƒ์—์„œ ๋ฐ›์€ ๋’ค์—๋งŒ apply๋ฅผ ์‹œ์ž‘ํ•˜๋ฉฐ, apply๋Š” ์ž๋™ ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ค€๋น„ ์ค‘ ๊ฐ Peer๋Š” ์ตœ๋Œ€ ๋„ค ์ด์›ƒ์— ๋™์‹œ์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ผ์‹œ์ ์ธ ์—ฐ๊ฒฐ ์‹คํŒจ๋Š” ๊ธฐ์กด ๋ช…๋ น ์ œํ•œ์‹œ๊ฐ„ ์•ˆ์—์„œ ์žฌ์‹œ๋„ํ•˜๋ฉฐ, ๋Œ€๊ธฐ ๊ฐ„๊ฒฉ์€ 250 ms๋ถ€ํ„ฐ ์ตœ๋Œ€ 2์ดˆ๊นŒ์ง€ ๋Š˜์–ด๋‚ฉ๋‹ˆ๋‹ค. ์ฒซ ์‹œ๋„์—์„œ libp2p dial backoff๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์žฌ์‹œ๋„ ์ „์— ํ•ด๋‹น ์ด์›ƒ์˜ backoff๋ฅผ ํ•œ ๋ฒˆ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„ ์‹œ๋„์—์„œ ๋ฐœ์ƒํ•œ backoff๋Š” ์œ ์ง€ํ•˜๋ฉฐ ๋‹ค๋ฅธ Peer๋‚˜ ์ „์—ญ ์—ฐ๊ฒฐ ์„ค์ •์€ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ทจ์†Œ ์‹œ ์žฌ์‹œ๋„ ๋Œ€๊ธฐ๋„ ์ข…๋ฃŒํ•˜๊ณ , ์„ค์ •ํ•œ transport ์—ฐ๊ฒฐ timeout์„ ๊ณ„์† ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„ backoff๊ฐ€ ๋ฐœ์ƒํ•ด๋„ Peer์˜ timeout ์ง„๋‹จ์—๋Š” ์•ž์„œ ํ™•์ธํ•œ ์‹ค์ œ ์—ฐ๊ฒฐ ์‹คํŒจ ์›์ธ์„ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. ๋„๋‹ฌํ•  ์ˆ˜ ์—†๋Š” Peer๋‚˜ ์—ฐ๊ฒฐยท์ž์› ํ•œ๋„ ๋ฌธ์ œ๋Š” ์žฌ์‹œ๋„๋งŒ์œผ๋กœ ํ•ด๊ฒฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Agent๋Š” ์ž˜๋ชป๋œ ์ž…๋ ฅ์— 400, ๋Œ€์ƒ ๊ฒ€์‚ฌ ์‹คํŒจยท์˜๊ตฌ Peer ์˜ค๋ฅ˜ยท์ž˜๋ชป๋œ ์ ์šฉ ์‘๋‹ต์— 502๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Peer์˜ ์ผ์‹œ์ ์ธ 503/504 ์‘๋‹ต์€ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•˜๊ณ , ์ผ์‹œ์ ์ธ ์ „๋‹ฌ ์˜ค๋ฅ˜๋Š” 503, ์ž์ฒด ์š”์ฒญ timeout์€ 504๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Peer๋Š” ์ž˜๋ชป๋œ ํ•„๋“œ์— 400, ์‹๋ณ„์žยท๊ธฐ๋Šฅ ํ—ˆ์šฉยท๊ณ„ํš ์ถฉ๋Œ์— 409, ์ทจ์†Œ์— 503, timeout์— 504๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋‘ endpoint ๋ชจ๋‘ ๋‚ด๋ถ€ ์ธ์ฆ(401)๊ณผ POST(405)๋ฅผ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค. Controller์˜ prepare ๋‹จ๊ณ„ ์˜ค๋ฅ˜์—๋Š” apply๊ฐ€ ์‹œ์ž‘๋˜์ง€ ์•Š์•˜์Œ์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ทจ์†Œ๋˜๊ฑฐ๋‚˜ ์‹คํŒจํ•œ apply๋„ ์ด๋ฏธ ์ ์šฉ๋˜์—ˆ์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋ถ„์‚ฐ rollback์€ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Controller์˜ topology_generated์—๋Š” ๋ชจ๋ธยทํŒŒ๋ผ๋ฏธํ„ฐยท์‹ค์ œ seedยท์ƒ์„ฑ๊ธฐ ๋ฒ„์ „ยท๊ทธ๋ž˜ํ”„ ํฌ๊ธฐ๋ฅผ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. topology_assignment๋Š” ๋…ธ๋“œ๋ณ„ ์˜ˆ์ • ์ด์›ƒยท์„ ํƒ์  ์ขŒํ‘œ์™€ evidence: "planned"๋ฅผ ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค. topology_stage๋Š” prepare/apply ์‘๋‹ต ์ˆ˜์™€ ์•„๋ž˜ ํ•„๋“œ๋ฅผ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. Peer์˜ topology_applied๋Š” ๋กœ์ปฌ์—์„œ ํ™•์ธํ•œ ๊ณ„ํš IDยทํ† ํ”ฝยท์ด์›ƒ์„ ํ•œ ๋ฒˆ ๊ธฐ๋กํ•˜๋ฉฐ ์ผ๋ฐ˜ telemetry์˜ ์ „๋‹ฌยท์œ ์‹ค ๊ทœ์น™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์ „์ฒด ์ด์›ƒ ๋ชฉ๋ก์€ ์ €์žฅ ๋กœ๊ทธ์— ๋ณด์กดํ•˜๊ณ  ์ตœ๊ทผ ์ด๋ฒคํŠธ ๋ชฉ๋ก์—์„œ๋Š” ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค. ํ™œ์„ฑ ๊ฐ„์„ ์€ ๊ณ„์† ๋Ÿฐํƒ€์ž„ ๋ฉ”์‹œ snapshot์œผ๋กœ ๊ด€์ธกํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…์€ internal/model/topology_command.go์— ์žˆ์Šต๋‹ˆ๋‹ค.

topology_stage ํ•„๋“œ ์˜๋ฏธ
attempts ํ•ด๋‹น ๋‹จ๊ณ„์—์„œ ์‹ค์ œ๋กœ ๋ณด๋‚ธ Controllerโ†’Agent HTTP ์š”์ฒญ ์ˆ˜์ด๋ฉฐ ์ตœ์ดˆ ์‹œ๋„์™€ ์žฌ์‹œ๋„๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
retries ์‹ค์ œ ์ถ”๊ฐ€ ์‹œ๋„ ์ˆ˜์ด๋ฉฐ apply๋Š” ํ•ญ์ƒ 0์ž…๋‹ˆ๋‹ค.
elapsedSeconds ํ•ด๋‹น prepare ๋˜๋Š” apply ๋‹จ๊ณ„ ์‹œ์ž‘๋ถ€ํ„ฐ ๊ฒฝ๊ณผํ•œ ์ดˆ์ž…๋‹ˆ๋‹ค.
timeoutSeconds ๋Œ€์ƒ ์„ ํƒ ์‹œ์ž‘์„ ๊ธฐ์ค€์œผ๋กœ ์ „์ฒด phase์— ์ ์šฉํ•œ ์œ ํšจ ํ—ˆ์šฉ ์‹œ๊ฐ„(์ดˆ)์ž…๋‹ˆ๋‹ค. ์ƒ์œ„ context ์ œํ•œ์‹œ๊ฐ„์ด ๋” ์งง์œผ๋ฉด ๊ทธ ๊ฐ’์„ ์šฐ์„ ํ•ฉ๋‹ˆ๋‹ค.
timeoutScope Timeout ์‹คํŒจ ์‹œ ์„ ํƒ์ ์œผ๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒด phase ์ œํ•œ์‹œ๊ฐ„ ๋งŒ๋ฃŒ๋Š” phase, ๊ฐœ๋ณ„ ์š”์ฒญ ์ œํ•œ์‹œ๊ฐ„ ๋งŒ๋ฃŒ ๋˜๋Š” Agent 504๋Š” command์ž…๋‹ˆ๋‹ค. ๋‘˜ ๋‹ค ํ•ด๋‹นํ•˜๋ฉด phase๋ฅผ ์šฐ์„ ํ•ฉ๋‹ˆ๋‹ค.

Profile update command

Scenario v3์˜ profile schedules๋Š” Agent์˜ POST /api/v1/nodes/{nodeId}/profile์„ ํ˜ธ์ถœํ•˜๊ณ  Agent๋Š” Peer POST /profile๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋‘ endpoint๋Š” ๋‚ด๋ถ€ ์ธ์ฆยทPOSTยท๋‹จ์ผ JSON ๊ฐ’์„ ์š”๊ตฌํ•˜๋ฉฐ ์•Œ ์ˆ˜ ์—†๋Š” ํ•„๋“œ๋ฅผ ๊ฑฐ๋ถ€ํ•˜๊ณ  ์š”์ฒญยท์‘๋‹ต์„ 32 KiB๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. Agent๋Š” X-KPL-Node-ID๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฒฝ๋กœ๋Š” ์ •ํ™•ํ•œ Runยทgenerationยท์ค€๋น„ ์ƒํƒœยทPeer ๊ฐœ์ฒด๋ฅผ ๊ฒ€์‚ฌํ•˜๊ณ  Agent๋Š” ์ „๋‹ฌ ์ „ํ›„ Run fence๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค.

์š”์ฒญ์€ {runId, generation, revision, stage, set}์ž…๋‹ˆ๋‹ค. revision์€ Run ์„ธ๋Œ€ ๋‚ด์—์„œ ์ฆ๊ฐ€ํ•˜๋Š” 1โ€“2147483647 ์ •์ˆ˜์ด๋ฉฐ stage๋Š” prepare ๋˜๋Š” apply์ž…๋‹ˆ๋‹ค. set์˜ ํƒ€์ž…๊ณผ ํ•„๋“œ๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค ๋ ˆํผ๋Ÿฐ์Šค์— ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. 200 ์‘๋‹ต์€ {nodeId, peerId, revision, stage, effective}์ด๋ฉฐ effective๋Š” ์š”์ฒญํ•œ ์„น์…˜์˜ ์™„์ „ํ•œ ๊ตฌ์ฒด์  ์„ค์ •์ž…๋‹ˆ๋‹ค. AgentยทController๊ฐ€ ์š”์ฒญ ๊ฐ’๊ณผ ๋Œ€์ƒ ์‹๋ณ„์ž๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. Prepare๋Š” ์ ์šฉ ์—†์ด ํ•œ ๊ฐœ์˜ ์ค€๋น„ ์„ค์ •์„ ๊ฒ€์ฆยท๋ณด๊ด€ํ•ฉ๋‹ˆ๋‹ค. Apply์—๋Š” ๋™์ผ revisionยทํŒจ์น˜์˜ ์ค€๋น„๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๊ณตํ•œ ๋™์ผ ์š”์ฒญ์€ ๋‹ค์‹œ ์‘๋‹ตํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์˜ค๋ž˜๋œ revision์ด๋‚˜ ๊ฐ™์€ revision์˜ ๋‹ค๋ฅธ ๊ฐ’์€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ์‹ ๊ทœ Peer๋Š” Run์˜ ์ตœ์‹  profile revision๊ณผ ํ•ด๋‹น ํŒจ์น˜๋ฅผ ์ƒ์†ํ•˜๋ฏ€๋กœ ์ง€์—ฐ๋œ ์ด์ „ ๋ช…๋ น์ด ์‹œ์ž‘ ์„ค์ •์„ ๋˜๋Œ๋ฆฌ์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

Controller๋Š” ์ „์ฒด ๋Œ€์ƒ์˜ prepare ์™„๋ฃŒ ํ›„ apply๋ฅผ ์‹œ์ž‘ํ•˜๋ฉฐ apply๋ฅผ ์ž๋™ ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋กœ์ปฌ ๋„คํŠธ์›Œํฌ ๋ณ€๊ฒฝ๊ณผ PubSub ๋ณ€๊ฒฝ์€ ๋ณ„๋„ ์—ฐ์‚ฐ์ด๋ฏ€๋กœ ๋‚˜์ค‘ ๋‹จ๊ณ„ ์‹คํŒจ ์‹œ Peer ์„ค์ •๋„ ์ผ๋ถ€๋งŒ ์ ์šฉ๋์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Peer๋Š” ์‹คํŒจ๋ฅผ ๊ธฐ๋กํ•˜๊ณ  ํ›„์† profile ๋ช…๋ น์„ ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์—ฐ์‚ฐ์€ 5์ดˆ, Peer ์š”์ฒญ์€ 8์ดˆ, Agent ์ „๋‹ฌ์€ 10์ดˆ, Controller ์ „๋‹ฌ์€ 12์ดˆ ํ•œ๋„์ด๋ฉฐ ์ทจ์†Œ์™€ ๊ฐ ๋ณ€๊ฒฝ์˜ timeout์ด ๋” ์งง์œผ๋ฉด ์ด๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. Peer๋Š” ์ž˜๋ชป๋œ ์ž…๋ ฅ 400, ์ถฉ๋Œยท์ ์šฉ ์˜ค๋ฅ˜ 409, ์ทจ์†Œ 503, ๊ธฐํ•œ ์ดˆ๊ณผ 504๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. Agent๋Š” ๊ฒ€์ฆยท์ „๋‹ฌยท์‘๋‹ต ์˜ค๋ฅ˜์— 502, ์ž˜๋ชป๋œ ์ž…๋ ฅ์— 400์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ธ์ฆยท๋ฉ”์„œ๋“œ ์˜ค๋ฅ˜๋Š” 401ยท405์ž…๋‹ˆ๋‹ค.

ํ™•์ธ๋œ Peer ์ƒํƒœ๋Š” profileRevision, runtimeProfile, profileAppliedAt์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Agent๋Š” ์‘๋‹ต์˜ ๋ณ€๊ฒฝ ์„น์…˜์„ ํ•ฉ์น˜๊ณ  ๋” ์ƒˆ๋กœ์šด Peer ์ƒํƒœ๋กœ ์œ ์‹ค๋œ ์‘๋‹ต์„ ๋ณต๊ตฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ถˆํ™•์‹คํ•œ apply๋Š” Agent์˜ profilePending: true๋ฅผ ์„ค์ •ํ•˜๋ฉฐ ๋„คํŠธ์›Œํฌ ์ ์šฉ๋„ ๋ถˆํ™•์‹คํ•˜๋ฉด ํ™•์ธ๋  ๋•Œ๊นŒ์ง€ networkPending: true๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด์ „์— ํ™•์ธํ•œ ์„ค์ •์€ ๊ณผ๊ฑฐ ๊ทผ๊ฑฐ์ด๋ฉฐ ์‹คํŒจํ•œ ๋ช…๋ น์˜ ๊ฒฐ๊ณผ๋ฅผ ์ฆ๋ช…ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ดํ›„ shaping ๋Œ€์ƒ์€ ๋‚ด๋ถ€ ์ƒ์„ฑ ์š”์ฒญ์˜ networkMutable: true๋กœ ์ปจํ…Œ์ด๋„ˆ NET_ADMIN์„ ์ค€๋น„ํ•˜๊ณ  ์ดˆ๊ธฐ ์ƒ์† revision์€ profileRevision์œผ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ์„ค์ • ์ด๋ฒคํŠธ๋Š” ์š”์ฒญ ๊ฐ’ยทํ™•์ธ ๊ฐ’ยท๋ถ€๋ถ„ ์‹คํŒจ๋ฅผ ๊ตฌ๋ถ„ํ•ด ๋ณด์กดํ•ฉ๋‹ˆ๋‹ค.

์ƒ์„ธ Peer ๋กœ๊ทธ

์ƒˆ Peer๋Š” events.jsonl์— upstream libp2p์—์„œ ์‹ค์ œ ์ œ๊ณตํ•˜๋Š” ๋ฉ”ํƒ€์ •๋ณด๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด publish/deliver/duplicate์™€ ๋ฉ”์ธ Metrics ์ง‘๊ณ„๋Š” ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. messageId๋Š” envelope์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ID ๋˜๋Š” raw์˜ pubsub-<hex native ID>์ด๋ฉฐ, fields.pubsubMessageId ๋ฐ ์•„๋ž˜ ์ƒ์„ธ ID ๋ชฉ๋ก์€ pubsub wire ID์˜ hex ํ‘œํ˜„์ž…๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ / ํ•„๋“œ ์˜๋ฏธ
send_*, recv_*, drop_*์˜ rpcMetadataVersion, rpcObservationId ๋ฉ”ํƒ€์ •๋ณด ํ˜•์‹ ๋ฒ„์ „ 1, ํ•œ ๋กœ์ปฌ RPC ์ฝœ๋ฐฑ์˜ ์—ฐ๊ฒฐ ID. ๊ฐ ์ด๋ฒคํŠธ์˜ eventId๋Š” ๋ณ„๋„๋กœ ์œ ์ง€
rpcMessageCount, rpcSubscriptionCount ๊ฐ™์€ RPC์— ํฌํ•จ๋œ ๋ฐ์ดํ„ฐ ๋ฉ”์‹œ์ง€ยท๊ตฌ๋… ํ•ญ๋ชฉ์˜ ์ „์ฒด ๊ฐœ์ˆ˜
IHAVE์˜ topicMessageIds ํ† ํ”ฝ๋ณ„ ๊ด‘๊ณ  ๋ฉ”์‹œ์ง€ ID ๋ชฉ๋ก
IWANT/IDONTWANT์˜ messageIds ์š”์ฒญยท๋น„์š”์ฒญ ๋ฉ”์‹œ์ง€ ID ๋ชฉ๋ก. ํ•ด๋‹น ํ”„๋กœํ† ์ฝœ ํ•ญ๋ชฉ์—๋Š” ์›๋ž˜ ํ† ํ”ฝ์ด ์—†์Œ
messageIdsComplete, omittedMessageIds ID ๋ชฉ๋ก ์™„์ „์„ฑ ๋ฐ ์ œํ•œ์œผ๋กœ ์ƒ๋žตํ•œ ๊ฐœ์ˆ˜. messageIdCount๋Š” ์ „์ฒด ๊ฐœ์ˆ˜
PRUNE์˜ peerExchangeIdsByTopic, peerExchangeIdsComplete, omittedPeerExchangeIds ์‹ค์ œ trace๊ฐ€ ์ œ๊ณตํ•œ ํ† ํ”ฝ๋ณ„ ๊ตํ™˜ Peer ID ๋ฐ ๋ชฉ๋ก ์™„์ „์„ฑ
rpc_metadata์˜ direction, messages, subscriptions send/recv/drop ๊ตฌ๋ถ„, {topic,pubsubMessageId} ๋ชฉ๋ก, {topic,subscribe} ๋ชฉ๋ก. ๋ฐ์ดํ„ฐยท๊ตฌ๋… ๋ฉ”ํƒ€์ •๋ณด๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ๋ณ„๋„ ์ด๋ฒคํŠธ ์ƒ์„ฑ
subscriptionsComplete, omittedSubscriptions ๊ตฌ๋… ๋ชฉ๋ก ์™„์ „์„ฑ ๋ฐ ์ƒ๋žต ๊ฐœ์ˆ˜
pubsub_reject์˜ reason, pubsubMessageId libp2p๊ฐ€ ๋ณด๊ณ ํ•œ ๋ฉ”์‹œ์ง€ ๊ฑฐ๋ถ€ ์‚ฌ์œ ์™€ ID
deliver / duplicate์˜ hopwaveMetadataVersion, hopwaveSource ๋ฒ„์ „ 1, ์ถœ์ฒ˜ wire. HopWave ๋ฉ”ํƒ€์ •๋ณด๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ๊ธฐ๋ก
hopwaveHopCountAvailable, hopwaveHopCount ์Œ์ˆ˜๊ฐ€ ์•„๋‹Œ wire ์นด์šดํ„ฐ์˜ ๊ฐ€์šฉ ์—ฌ๋ถ€. ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์œผ๋ฉด ๊ฐ’์€ ์ƒ๋žต. HopWave ์ „๋‹ฌ์—์„œ๋Š” ์ ˆ๋Œ€ ๊ฒฝ๋กœ ๊ธธ์ด๊ฐ€ ์•„๋‹Œ ์ˆœํ™˜ ์นด์šดํ„ฐ
hopwavePropagationType ์žˆ๋Š” wire enum์„ eager-push, lazy-pull, unknown์œผ๋กœ ํ‘œํ˜„. enum์ด ์—†์œผ๋ฉด ์ƒ๋žต
timestampSource, sourceTimestamp trace ์‹œ๊ฐ์˜ ์ถœ์ฒ˜์™€ ๋ณด์ • ์ „ ์›๋ณธ ์‹œ๊ฐ. upstream ์‹œ๊ฐ์ด ์—†์œผ๋ฉด peer-clock์œผ๋กœ ํ‘œ์‹œํ•˜๊ณ  ์›๋ณธ ์‹œ๊ฐ์„ ๋งŒ๋“ค์ง€ ์•Š์Œ
clockBasis, clockOffsetMs, clockUncertaintyMs ์œ ํšจํ•œ Controller ๋™๊ธฐํ™” ๊ทผ๊ฑฐ๊ฐ€ ์žˆ์„ ๋•Œ ์ถ”๊ฐ€ํ•˜๋Š” ์‹œ๊ฐ ๋ณด์ • ์ •๋ณด

ID ์ƒ์„ธ๋Š” ์ข…๋ฅ˜๋ณ„ ์ตœ๋Œ€ 8,192๊ฐœ ๋ฐ hex ํ•ฉ๊ณ„ 512KiB, ๊ตฌ๋… ๋ชฉ๋ก์€ ์ตœ๋Œ€ 8,192๊ฐœ์ž…๋‹ˆ๋‹ค. ์ƒ์„ธ ๋ชฉ๋ก์ด ์ž˜๋ ค๋„ ํ•ด๋‹น ์ด๋ฒคํŠธ์˜ RPCยทentryยทID ์ง‘๊ณ„๋Š” ์ „์ฒด ๊ฐœ์ˆ˜๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ์œ ์‹ค์€ ๊ธฐ์กด telemetry_drop๊ณผ ์†Œ์Šค sequence๋กœ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค. ์›๋ณธ payload, IWANT ๋ฐœ์‹  ์›์ธ, wire์— ์—†๋Š” RPC ์ „์—ญ ID๋Š” ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Eager/Lazy๋Š” ์ง€ํ‘œ ๋ฌธ์„œ์˜ ๋ฉ”ํƒ€์ •๋ณด ์ถ”์ • ๊ทœ์น™์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.

hopwave* ํ•„๋“œ๋Š” ์„œ๋ช…ํ•˜์ง€ ์•Š๋Š” wire ๋ณด๊ณ ๊ฐ’์ž…๋‹ˆ๋‹ค. ํ•ด์„๊ณผ ํ˜ธํ™˜ ์š”๊ฑด์€ HopWave๋ฅผ ๋”ฐ๋ฅด๋ฉฐ ๊ธฐ์กด ๋ถ„์„ ์ถ”์ •์€ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. Wire ๋ฉ”ํƒ€์ •๋ณด๊ฐ€ ์—†๋Š” ๋กœ๊ทธ์—๋Š” HopWave ํ•„๋“œ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ธ์ฆ

Controller์™€ ๋ชจ๋“  Agent์— ๊ฐ™์€ KPL_USERยทKPL_PASSWORD๋ฅผ ์„ค์ •ํ•˜๋ฉฐ ์‹œ์ž‘ ์‹œ ๋‘˜ ๋‹ค ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. ๋Œ€์‹œ๋ณด๋“œ๋Š” ๋จผ์ € Log in ํ™”๋ฉด์„ ์—ด๊ณ , ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ์ž„์˜์˜ ์„œ๋ฒ„ ์„ธ์…˜์„ ์ƒ์„ฑํ•ด HttpOnlyยทSameSite=Strict ์ฟ ํ‚ค๋กœ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค. HTTPS์—์„œ๋Š” Secure ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” reverse proxy์˜ X-Forwarded-Proto: https๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์„ธ์…˜์€ 12์‹œ๊ฐ„ ๋’ค ๋งŒ๋ฃŒ๋˜๊ณ  Log out ๋˜๋Š” Controller ์žฌ์‹œ์ž‘ ์‹œ ๋ฌดํšจํ™”๋ฉ๋‹ˆ๋‹ค. ๋กœ๊ทธ์•„์›ƒยท๋งŒ๋ฃŒ ์‹œ ํ•ด๋‹น ์„ธ์…˜์˜ SSE ์—ฐ๊ฒฐ๋„ ๋‹ซ์ง€๋งŒ ์‹คํ–‰ ์ค‘ ์‹คํ—˜๊ณผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋ถ„์„์€ ๊ณ„์†๋ฉ๋‹ˆ๋‹ค. ์„ค์ • ๊ณ„์ • ํ•˜๋‚˜๊ฐ€ ๋Œ€์‹œ๋ณด๋“œ ์ „์ฒด ๊ถŒํ•œ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

๋Œ€์‹œ๋ณด๋“œ/API ์กฐํšŒยท๋ณ€๊ฒฝ, SSE, ์‹œ๋‚˜๋ฆฌ์˜ค ๊ฒ€์ฆ, ๋ถ„์„, ๋‹ค์šด๋กœ๋“œ๋Š” ๋ชจ๋‘ ๋กœ๊ทธ์ธ ์„ธ์…˜์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ฏธ์ธ์ฆ API๋Š” JSON 401์„ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, HTML์„ ์š”์ฒญํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ € GET/HEAD ํƒ์ƒ‰์—๋Š” ๊ฐ™์€ 401 ์ƒํƒœ์˜ Login required ์•ˆ๋‚ด ํŽ˜์ด์ง€๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. /login-required์—์„œ๋„ ์—ด ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋กœ๊ทธ์ธ ํ™”๋ฉด๊ณผ ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ํ•˜๋Š” ๋งํฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€์‹œ๋ณด๋“œ API/SSE์˜ ์„ธ์…˜ ๋งŒ๋ฃŒ ์‹œ์—๋Š” ์ด ํŽ˜์ด์ง€๋กœ ํ•œ ๋ฒˆ ์ด๋™ํ•˜๊ณ  ์žฌ์—ฐ๊ฒฐ์„ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ๋กœ๊ทธ์•„์›ƒํ•˜๋ฉด /login์œผ๋กœ ์ด๋™ํ•˜๊ณ , ์„ธ์…˜ ์—†์ด ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์ฒ˜์Œ ์—ด ๋•Œ๋„ ๊ธฐ์กด์ฒ˜๋Ÿผ /login์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•ฉ๋‹ˆ๋‹ค. ์„ธ์…˜์„ ์‚ฌ์šฉํ•˜๋Š” ๋ณ€๊ฒฝ ์š”์ฒญ์€ X-KPL-Request: dashboard ํ—ค๋”๋„ ํ•„์š”ํ•˜๋ฉฐ ๋‹ค๋ฅธ origin์˜ ์š”์ฒญ์€ ๊ฑฐ๋ถ€ํ•ฉ๋‹ˆ๋‹ค. UI๋Š” ์ด ํ—ค๋”๋ฅผ ์ž๋™์œผ๋กœ ๋„ฃ์œผ๋ฏ€๋กœ ์ถ”๊ฐ€ ํ† ํฐ ์ž…๋ ฅ์ด ์—†๊ณ  ๋น„๋ฐ€๋ฒˆํ˜ธยท์„ธ์…˜ ์ฟ ํ‚ค๋ฅผ localStorage์— ์ €์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ์€ JSON {user, password}๋งŒ ๋ฐ›์œผ๋ฉฐ ์ž˜๋ชป๋œ ๊ณ„์ •์€ 401, ๊ฐ™์€ ์ ‘์† IP์—์„œ 5๋ถ„ ๋™์•ˆ 10ํšŒ ์‹คํŒจํ•œ ์ดํ›„์—๋Š” 429๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์„ธ์…˜๊ณผ ์‹คํŒจ ๊ธฐ๋ก์€ ๊ฐ๊ฐ ์ตœ๋Œ€ 1,024๊ฐœ์ž…๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ์‘๋‹ต ์ „์†ก ์ค‘์—๋Š” ์„ธ์…˜ ์ž ๊ธˆ์„ ์œ ์ง€ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ์‘๋‹ต์„ ๋ฐ›์ง€ ๋ชปํ•˜๋Š” ๋กœ๊ทธ์ธ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋‹ค๋ฅธ ์„ธ์…˜ ํ™•์ธ์ด๋‚˜ ๋กœ๊ทธ์•„์›ƒ์„ ๋ง‰์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ ๋ฆฌ์†Œ์Šค, health/์‹œ๊ฐ endpoint, /metrics, ๋‘ Prometheus target-discovery endpoint๋งŒ ๊ณต๊ฐœ๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋‚ด๋ถ€ Agent ๋“ฑ๋กยทheartbeatยท์ด๋ฒคํŠธ์™€ Peer bootstrapยทdiscovery๋Š” ๊ณ„์ • ์„ค์ •์—์„œ ์šฉ๋„๋ฅผ ๋ถ„๋ฆฌํ•ด ์ž๋™ ์ƒ์„ฑํ•œ ์„œ๋น„์Šค ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. Peer์—๋Š” ๋กœ๊ทธ์ธ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋Œ€์‹  ์ด ํ‚ค๋งŒ ์ „๋‹ฌํ•˜๋ฉฐ, ํ‚ค๋กœ ๋Œ€์‹œ๋ณด๋“œ ๊ฒฐ๊ณผ API์— ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ ์‹คํ—˜์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. KPL_API_TOKEN๊ณผ --token CLI ์˜ต์…˜์€ ํ๊ธฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž๊ฒฉ ์ฆ๋ช… ๋ณ€๊ฒฝ ์‹œ ์ด์ „ ํ‚ค๋ฅผ ์“ฐ๋Š” ์ง„ํ–‰ ์ค‘ ์‹คํ—˜ยท์ž”์กด Peer๋ฅผ ์ •๋ฆฌํ•œ ๋’ค Controller์™€ Agent๋ฅผ ํ•จ๊ป˜ ์žฌ๋ฐฐํฌํ•˜์‹ญ์‹œ์˜ค. Grafana ๋กœ๊ทธ์ธ์€ ๋ณ„๋„์ž…๋‹ˆ๋‹ค.

REST ํด๋ผ์ด์–ธํŠธ๋Š” ์•„๋ž˜ ์ฃผ์†Œ๋ฅผ access ์ถœ๋ ฅ์œผ๋กœ ๋ฐ”๊พธ๊ณ  ํ•œ ๋ฒˆ ๋กœ๊ทธ์ธํ•ฉ๋‹ˆ๋‹ค. ๋น„๊ณต๊ฐœ ์ฟ ํ‚ค ํŒŒ์ผ์„ ์œ„ ์˜ˆ์ œ๋“ค์—์„œ ์žฌ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค.

KPL_URL=http://control-node:8080
KPL_COOKIE_JAR=$(mktemp)
read -r -p 'Username: ' KPL_USER
read -r -s -p 'Password: ' KPL_PASSWORD; printf '\n'
export KPL_USER KPL_PASSWORD
python3 -c 'import json,os; print(json.dumps({"user":os.environ["KPL_USER"],"password":os.environ["KPL_PASSWORD"]}))' |
  curl --fail-with-body -c "$KPL_COOKIE_JAR" \
    -H 'Content-Type: application/json' --data-binary @- "$KPL_URL/api/v1/auth/login"
unset KPL_PASSWORD
curl --fail-with-body -b "$KPL_COOKIE_JAR" "$KPL_URL/api/v1/snapshot"

์š”์ฒญ์„ ๋งˆ์นœ ๋’ค ๋กœ๊ทธ์•„์›ƒํ•˜๊ณ  ์ฟ ํ‚ค ํŒŒ์ผ์„ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

curl --fail-with-body -b "$KPL_COOKIE_JAR" -H 'X-KPL-Request: dashboard' \
  -X POST "$KPL_URL/api/v1/auth/logout"
rm -f "$KPL_COOKIE_JAR"

HTTP ์ž์ฒด๋Š” ์ž๊ฒฉ ์ฆ๋ช…๊ณผ ์ฟ ํ‚ค๋ฅผ ์•”ํ˜ธํ™”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. HTTPS ๋˜๋Š” ์‹ ๋ขฐ ๊ด€๋ฆฌ๋ง์„ ์‚ฌ์šฉํ•˜๊ณ  HTTPS proxy๋Š” forwarded protocol ํ—ค๋”๋ฅผ ๋ฎ์–ด์จ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜ค์˜ ๊ฐ™์€ ๋„ค ๊ฐ€์ง€ job counter๊ฐ€ /api/v1/snapshot๊ณผ SSE snapshot์—๋„ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ๋Œ€์‹œ๋ณด๋“œ๋Š” ๊ฐ run์— ์ด๋ฅผ ํ‘œ์‹œํ•˜๋ฏ€๋กœ Controller ๋กœ๊ทธ๋ฅผ ์—ด์ง€ ์•Š์•„๋„ ์‹คํ–‰ ์ค‘, ์„ฑ๊ณต, ์‹คํŒจ, ์ทจ์†Œ๋œ background ์ž‘์—… ์ˆ˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‹œ๊ฐํ™” ๊ฐ€์ด๋“œ์—์„œ ๋ถ„์„ ์‘๋‹ต, ๊ทธ๋ž˜ํ”„์™€ ๋‚ด๋ณด๋‚ด๊ธฐ ํ˜•์‹์„ ํ™•์ธํ•˜์‹ญ์‹œ์˜ค.

Bandwidth ์ธก์ • ยท v2 ์ „์ฒด ๋ถ„์„ ๋Œ€์กฐ

โš ๏ธ **GitHub.com Fallback** โš ๏ธ