enode — API 표면
f46e187시점의 구현이 실제로 서빙 · 노출하는 표면 전수. 정본
(mediator-api.md)과의 대조는 마지막 절에 관측으로만 적었다.
Mediator HTTP 표면 — 15개 라우트
라우트 정의가 흩어져 있지 않다. 15개 전부가 internal/api/api.go:53 Handler()
안의 단일 http.ServeMux 에 등록된다(:55-69). 라우터 라이브러리 의존 0 —
표준 net/http 의 Go 1.22+ 메서드+경로 패턴만 쓴다.
runctl → Mediator (제출 · 조회 · 취소)
| 메서드 · 경로 | 등록 | 핸들러 |
|---|---|---|
POST /v1/runs |
api.go:58 |
postRuns api.go:318 → submit api.go:325 |
POST /v1/runs/dry-run |
api.go:59 |
postDryRun api.go:323 → 같은 submit |
GET /v1/runs/{id} |
api.go:60 |
getRun api.go:454 |
GET /v1/runs/{id}/record |
api.go:65 |
getRecord api.go:787 |
POST /v1/runs/{id}/cancel |
api.go:66 |
postCancel api.go:595 |
POST /v1/runs 와 POST /v1/runs/dry-run 은 같은 파서와 같은 매처를 쓴다 —
둘 다 submit(w, r, dry) 를 부른다. dry-run 이 실제 제출과 다른 판정을 내리지
않게 하는 배치다.
GET /v1/runs/{id} 는 steps 를 함께 준다(ADR-025, api.go:120-138 runView
· :141 view → internal/store/observe.go:45 Steps).
GET /v1/runs/{id}/record 는 tar 를 스트리밍한다. 봉인 전이면 409.
되묻기 (ADR-032)
| 메서드 · 경로 | 등록 | 핸들러 |
|---|---|---|
GET /v1/asks |
api.go:62 |
getAsks api.go:473 |
POST /v1/runs/{run}/steps/{seq}/answer |
api.go:63 |
postAnswer api.go:497 |
인박스(GET /v1/asks)가 정본이고 웹훅 푸시는 보조다.
enode → Mediator (당기기)
| 메서드 · 경로 | 등록 | 핸들러 |
|---|---|---|
POST /v1/nodes |
api.go:55 |
postNodes api.go:167 |
POST /v1/nodes/{id}/claim |
api.go:56 |
postClaim api.go:226 |
POST /v1/runs/{run}/steps/{seq}/result |
api.go:57 |
postResult api.go:267 |
PUT /v1/runs/{run}/steps/{seq}/log |
api.go:67 |
putLog api.go:618 |
PUT /v1/runs/{run}/steps/{seq}/blob/{name} |
api.go:68 |
putBlob api.go:668 |
GET /v1/runs/{run}/blob/{name} |
api.go:69 |
getBlob api.go:754 |
주의할 점 넷:
- 별도의 하트비트 표면이 없다.
POST /v1/nodes광고가 곧 하트비트이고,
응답이 유효 임대 목록 전체(leases)와renew_seconds를 나른다
(ADR-016·ADR-028,api.go:160-166advertResponse·:207-216) - 표면에서 비멱등인 곳은
claim하나뿐이다.postClaim→
internal/store/claim.go:246ClaimStep - claim 은 롱폴이다. 1초 티커로 최대 7,200초 대기하고, 줄 단계가 없으면
204 No Content. 서버가WriteTimeout을 걸지 않고
(cmd/mediator/main.go:114-117) 클라이언트도 타임아웃 없는&http.Client{}
를 쓴다(cmd/enode/main.go:132-136) - blob 경로가 비대칭이다. 올릴 때는 단계를 두 세그먼트로 가리키고
(/runs/{run}/steps/{seq}/blob/{name}), 내릴 때는 Run 단위로 가리킨다
(/runs/{run}/blob/{name}). 이유가api.go:653-666주석에 있다
PUT blob 은 계약이 그 단계에 스키마를 걸어 두었으면
internal/schema 로 검증하고, 실패하면 422 이고 저장하지 않는다
(api.go:717-731, schemaFor :777).
어휘 · 관측
| 메서드 · 경로 | 등록 | 핸들러 |
|---|---|---|
GET /v1/capabilities |
api.go:61 |
getCapabilities api.go:580 |
GET /v1/runs/{id}/ledger |
api.go:64 |
getLedger api.go:558 |
GET /v1/capabilities 는 존재만 답하고 여유는 답하지 않는다.
internal/store/store.go:392 Capabilities 주석이 "함대 전체의 합집합" 이라는
한계를 적고 여유 판단은 dry-run 으로 넘긴다.
GET /v1/runs/{id}/ledger 는 목록이지 본문이 아니다.
인증과 식별
15개 라우트 전부가 s.auth(...) 로 감싸여 있다. 예외 경로 0개 —
헬스체크도 메트릭도 인증 밖에 있지 않다.
- 인증:
Authorization: Bearer <token>을crypto/subtle상수시간 비교
(api.go:83auth) - 식별:
X-Enode-Principal헤더. 검증하지 않는다(api.go:97principal).
인증과 식별을 일부러 분리했다
오류 코드 체계
api.go:110 fail 이 단일 출구다.
| 코드 | 뜻 | 발원지 |
|---|---|---|
| 400 | 계약 문법 위반 | internal/contract/contract.go:954 Validate |
| 401 | 토큰 불일치 | api.go:83 auth |
| 404 | Run · 단계 · blob 없음 | 각 핸들러 |
| 409 | 전부 사용 중 (일시) · 봉인 전 record 요청 | internal/match/match.go:24 CodeAllBusy · api.go:795-798 |
| 410 | 봉인 뒤의 쓰기 | api.go:639-642 (putLog) · :686-689 (putBlob) |
| 413 | blob 크기 초과 | internal/config MaxBlobBytes 기본 10 MiB |
| 422 | 후보 없음 (영구) · 스키마 위반 | internal/match/match.go:23 CodeNoCandidate · api.go:717-731 |
| 503 | Record 저장소 미구성 | api.go:45-51 needRecords |
409 와 422 의 구분이 이 표면의 의미론적 핵심이다. 422 는 영구 거절(그런 노드가
함대에 아예 없다), 409 는 일시 거절(있는데 지금 다 바쁘다)이다. 클라이언트가
재시도해야 하는지가 여기서 갈린다.
CLI 표면 — runctl (8 하위명령)
cmd/runctl/main.go:124-310 디스패치, usage() 는 :402.
| 하위명령 | 하는 일 | 부르는 라우트 |
|---|---|---|
submit |
계약 제출 | POST /v1/runs |
dry-run |
배정만 계산 | POST /v1/runs/dry-run |
status |
Run 과 단계 상태 | GET /v1/runs/{id} |
record |
Record tar 내려받기 | GET /v1/runs/{id}/record |
cancel |
Run 취소 | POST /v1/runs/{id}/cancel |
capabilities |
함대 어휘 | GET /v1/capabilities |
asks |
미답 질문 목록 | GET /v1/asks |
answer |
질문에 답 | POST /v1/runs/{run}/steps/{seq}/answer |
종료코드 4단 (cmd/runctl/main.go 상단 상수):
0 성공
1 Run 실패 (요청은 정상 처리됐고 Run 이 FAILED)
2 요청 거절 (4xx)
3 Mediator 도달 불가
permute() 가 플래그를 앞으로 모아 runctl submit x.json --wait 같은 자연스러운
순서를 받는다.
CLI 표면 — enodectl (8)
cmd/enodectl/main.go:44-70. HTTP 를 부르지 않는다 — 한 기계의 enode 프로세스만
관리한다. 셸 스크립트판이 node_id 를 다시 계산하던 것을 없애려고 Go 로 옮겼다.
setup list id start stop logs status --version
프로세스 조작의 OS 분기는 proc_unix.go / proc_windows.go 쌍이 맡는다.
CLI 표면 — enode
cmd/enode/main.go:29-46.
- 하위명령:
hook stop(R6의 종료 훅이 곧 enode 자기 자신이다) ·setup·
--version - 그 외에는 플래그 기반 상주 실행:
--config·--mediator·--token·--every·--debug·--once·
--ready-file
--once 와 --ready-file 이 탄력 노드(단발 기동)용이다.
hook stop 은 어떤 실패에도 종료코드 0 을 돌려준다
(cmd/enode/hook.go:53) — 훅이 하네스 종료를 막지 않게 하려는 것이다.
아웃바운드 — asks 웹훅
Mediator 가 바깥으로 거는 유일한 호출이다.
- 위치:
internal/store/ask.go:102PushAsks - 대상: 설정된
notify.asks_url - 형태:
AskEventJSON 하나를 POST - 재시도 없다.
go func()으로 쏘고 결과를 기다리지 않는다. 프로세스가
즉시 죽으면 유실된다 — 인박스(GET /v1/asks)가 정본이고 푸시는 보조라는
설계에서 허용된다
클라이언트 측 사용 현황
15개 라우트 전부에 최소 하나의 1차 클라이언트가 있다.
| 클라이언트 | 부르는 라우트 수 | 위치 |
|---|---|---|
internal/runctl/client.go |
8 | :118,177,187,197 외 |
internal/enode/{claim,advertise}.go |
6 | advertise.go:57 · claim.go:114,143,170,198,242 |
cmd/iapadapter/mediator.go |
7 | 어댑터 자체 HTTP |
한 가지 비대칭: GET /v1/runs/{id}/ledger 는 cmd/iapadapter/mediator.go:271
만 부르고 runctl 에는 대응 하위명령이 없다.
관측 — 문서의 표면 개수와 구현의 표면 개수
판정이 아니라 관측이다. 아래를 근거로 다음 스테이지가 판단한다.
구현이 서빙하는 것: 15
internal/api/api.go:55-69. 전수 확인이 grep 하나로 끝난다.
문서를 메서드+경로로 grep 하면: 21
mediator-api.md 를
grep -oE '(GET|POST|PUT|DELETE|PATCH) +/v1[A-Za-z0-9/{}_.-]*' | sort -u 로
세면 서로 다른 문자열 21개가 나온다(백틱 안쪽만 세면 16 — 패턴에 따라 수가
달라진다). 그 21개의 구성:
| 수 | 무엇 | 근거 |
|---|---|---|
| 15 | 구현이 서빙하는 라우트와 1:1 대응 | — |
| 2 | ADR-021 이전의 한 세그먼트 표기가 § 제목에 남은 것: POST /v1/steps/{id}/result, PUT /v1/steps/{id}/log |
제목 line 385 · 418, 대응표 line 563 · 564. 같은 문서 line 238-241 의 목록은 두 세그먼트이고 구현도 두 세그먼트다 |
| 2 | 자리표시자 이름만 다른 변형: {id} 대 {run}(blob 라우트), 그리고 § 제목이 GET/PUT 을 한 줄로 묶으면서 생긴 표기 |
line 193 · 432 · 565 대 line 239 · 240 |
| 1 | §7 "없는 것 — 일부러" 에서 명시 기각: GET /v1/nodes?free=… |
line 672 |
| 1 | "자리만 잡는다" 라고 적은 예약 자리: GET /v1/runs?state=&since= |
line 529 |
15 + 2 + 2 + 1 + 1 = 21.
따라서: 기각 · 예약 · 표기중복을 뺀 문서의 실제 표면 집합은 구현이 서빙하는
15개와 정확히 같다.
별개의 관측: 세 문장이 세 가지 수를 든다
정본 안에서 표면 개수를 말하는 문장 셋이 서로 다르다.
system.md §5 "Mediator 표면 12개 전부"
mediator-api.md §6.3 "표면 12 → 13" (G3 항목)
enode/README.md "표면 13개를 먼저 고정했다"
셋 다 기록한다. 어느 것이 맞는지, 무엇을 고칠지는 이 문서가 판정하지 않는다.
없는 것
- OpenAPI / Swagger 없음. 정본 문서가 "OpenAPI 로 굳히지 않는다" 를 명시했다
- godoc 외의 생성 문서 없음
GET /v1/nodes계열 없음 — §7 이 일부러 기각한 것/retry·/queue없음