문서와 심볼

aidlc-docs 를 그대로 읽는다 · 읽기 전용
/home/sunny/enode-fixup-workshop2-visualizer연결 중
construction/u-queue/functional-design/domain-entities.md204 줄

U-대기열 — 타입과 자리

이 유닛은 스키마를 한 줄도 안 바꾼다. 새 열 0 · 새 테이블 0 · 새 인덱스
0 · 새 라우트 0 이다. runs.state 에 QUEUED 가 실제로 들어가고, 그 행이
다시 RUNNING 으로 움직이는 것이 전부다.

질문 넷의 답은 A · A · A · A 다 (functional-design-questions.md).

읽는 순서 — 이 문서가 무엇이 어떤 모양으로 있는가이고, 절차는
business-logic-model.md, 규칙과 완료 조건은 business-rules.md 에 있다.


1. 스키마 — 안 바꾼다

1.1 QUEUED 는 마이그레이션이 아니다

runs.state 에 CHECK 가 없다 (schema.sql). 어휘가 늘 때 마이그레이션을
강요하지 않기로 한 결정이 여기서 값을 낸다. U-바닥이 store.StateQueued
상수와 ValidState 의 다섯째 값을 이미 놓았다 (internal/store/list.go:38).

1.2 인덱스를 안 더한다

ListQueued 가 WHERE state = $1 ORDER BY created_at, run_id 로 훑는다.
인덱스를 더하지 않는 근거는 U-바닥의 domain-entities.md §1.4 와 같다 —
runs 는 v1 규모에서 작고, 대기열은 그중에서도 부분집합이며, 훑기는 자원이
풀린 사건에만 돈다(주기가 아니다). 느려지는 것이 보이면 그때 재는 것이
지금 짐작해서 거는 것보다 낫다.


2. QUEUED Run 의 행 모양

제출 시점에 이 모양으로 완성된다. 승격이 채우는 것은 아래 표의 굵은
칸 넷뿐이다.

테이블 열 QUEUED 일 때 승격 뒤
runs state QUEUED RUNNING
contract 제출 전문 그대로
contract_versions [] 그대로
principal 제출자 그대로
work_id 계약의 work 에서 유도 그대로
assigned NULL [{as, nodes:[{node,label}]}]
reject NULL NULL
verdict · ended_at NULL NULL
created_at 제출 시각. FIFO 의 기준이다 그대로
steps state 전부 PENDING 그대로
node_id 빈 문자열 역할의 노드
kind · needs 제출 시점에 확정 그대로
attempt 0 그대로
started_at · ended_at · result NULL 그대로
chosen false 그대로
leases 행이 없다 역할이 쥔 노드마다 하나
Record 디렉터리 열려 있다 그대로

세 가지가 이 표에서 곧바로 따라 나온다.

① 아무 노드도 못 집는다. ClaimStep 이 WHERE s.node_id = $1 로 자기
몫만 보므로 빈 문자열인 단계는 어느 노드의 질의에도 안 걸린다.
승격이 그 열을 채우는 것이 곧 배분이다 — internal/store/claim.go 의
「당기기가 이미 배분이다」가 그대로 성립하고, 배분 정책은 여전히 0 개다.

② Reap 이 안 쓸어간다. 회수 UPDATE 가
run_id IN (SELECT run_id FROM leases WHERE not_after <= now()) 로 걸러지는데
QUEUED 는 임대를 안 쥐므로 그 부분질의에 안 들어온다. 뒤이은 두 문장도
state='FAILED' 이거나 종료 상태만 본다. 대기 상한이 없다는 결정이 코드에서
이미 참이다.

③ 대기 중 취소가 봉인까지 닿는다. Record 가 제출 시점에 열려 있고
Cancel 이 state NOT IN ('SUCCEEDED','FAILED') 로 거르므로 QUEUED 가
그대로 걸린다. steps 를 PENDING 에서 FAILED 로 옮기고, 없는 임대를
지우고, internal/store/reap.go:207 에서 봉인한다. 코드를 안 고친다.


3. 새로 생기는 자리

3.1 Store 필드 하나 — LeaseTTL

requirements.md §4.1 이 store.WakeQueued(ctx) error 로 인자 없는
시그니처를 고정했다.
승격이 발급할 임대의 not_after 를 인자로 못 받으므로
배선으로 받는다.

type Store struct {
    // ...
    // MaxLeasesPerRun 은 한 Run 이 동시에 쥘 수 있는 노드 수다 (ADR-024 §4.2).
    MaxLeasesPerRun int
    // LeaseTTL 은 승격이 발급하는 임대의 수명이다.
    //
    // 인자가 아니라 필드인 이유 — WakeQueued 는 인자를 안 받는다.
    // 부르는 자리가 여섯이고 그중 둘(Reap · 기동)은 설정을 손에 안 쥐고 있다.
    // 0 이면 승격을 안 한다 — 조용히 무한 임대를 발급하는 것보다 낫다.
    LeaseTTL time.Duration
}

MaxLeasesPerRun 이 이미 같은 방식으로 배선돼 있다 (cmd/mediator/main.go
의 openRecords). 새 방식을 만들지 않고 그 옆에 둔다.

값은 cfg.Lease.TTLSeconds 다 — submit 이 notAfter 를 계산하는 데 쓰는
그 값이고, 두 경로가 같은 수명을 발급해야 한다.

3.2 옮기는 함수 하나 — label

Q1 의 답 A 가 승격을 store 에 둔다. 승격도 제출도 []match.Assignment 를
[]store.Assigned 로 옮겨야 한다.

   지금    internal/api/api.go:849    func label(as, adverts) []store.Assigned
   뒤      internal/store/list.go     func Label(as, adverts) []Assigned

두 벌로 두면 Record 의 노드 이름이 경로마다 갈린다. label 이 붙이는
것은 사람이 읽는 이름이고, 그것이 없으면 봉인된 Record 에서 「서로 다른
기계였다」를 사람이 못 읽는다 (ADR-015 §2). 제출로 들어온 Run 과 승격으로
들어온 Run 이 그 점에서 다르면 안 된다.

internal/api 는 internal/store 를 이미 임포트하므로 부르는 쪽은
store.Label(...) 한 줄이 된다. 경계 검사기의 금지 넷에 이 방향은 없다.

3.3 새 함수 다섯

// internal/store/queue.go — U-바닥이 만든 파일에 이어 붙인다
func (s *Store) WakeQueued(ctx context.Context) error
func (s *Store) UnavailableNodes(ctx context.Context) (map[string]bool, error)
func (s *Store) promote(ctx context.Context, q QueuedRun,
    adverts []contract.Advert, blocked map[string]bool) ([]string, error)   // 비공개

// internal/store/store.go
func (s *Store) CreateQueuedRun(ctx context.Context, r Run, steps []contract.Step) error
func (s *Store) insertSteps(ctx context.Context, tx pgx.Tx, runID string,
    steps []contract.Step, nodeOf map[string]string) error                  // 비공개

// internal/api/queue.go — 신설
func (s *Server) wake(ctx context.Context, why string)

promote 가 []string 을 내는 것은 그 Run 이 쥔 노드다. 훑는 쪽이 같은
훑기 안에서 그 노드를 blocked 에 더해야 뒤의 Run 이 같은 노드를 다시
고르지 않는다. 기본키 충돌이 이중 배정을 막아 주긴 하지만, 그것은 마지막
방어선이지 설계가 아니다 — 헛도는 트랜잭션이 줄고 FIFO 가 뜻대로 돈다.

insertSteps 는 CreateRun 과 CreateQueuedRun 이 같이 쓴다. 그 안에
st.Kind() 검증과 contract.NeedsOf 정규화가 있고, 두 벌이 되면 제출
경로마다 다른 정규화가 걸린다.

UnavailableNodes 는 BusyNodes 와 DrainingNodes 의 합집합이다 (FR-11 의
중앙 쪽). submit 과 WakeQueued 가 같은 것을 봐야 한다 — 제출은 draining
노드를 후보에서 빼는데 승격은 안 뺀다면, 대기하던 Run 이 회수 중인 노드로
간다.


4. 와이어 — 202

{
  "run_id": "…",
  "state": "QUEUED",
  "steps": [ { "seq": 1, "name": "…", "state": "PENDING", "node": "", … } ],
  "warnings": [ "…" ]
}

runView 그대로다. assigned · reject · verdict 는 omitempty 라 빠진다.

제출 응답과 조회 응답이 같다. requirements.md §3.1 이
GET /v1/runs/{id} 의 모양을 이렇게 고정했고, 둘이 다르면 읽는 쪽이 형태를
둘 알아야 한다. warnings 는 201 과 같게 싣는다 — 경고는 계약 저자가 읽고
고칠 자리이고 (ADR-061 §2), 대기에 들어갔다고 안 보여 줄 이유가 없다.


5. 안 만드는 것

   READY 상태                   NFR-I2.  ADR-064 §1.1 이 기각했다.  QUEUED 하나만 는다

   대기열 테이블                 runs.state 가 대기열이다.  따로 두면 두 벌이 되고,
                                크래시 복구 검증 대상이 하나 는다

   우선순위 열 · 정책 틀          constraints.md §2.  조건은 도착 순서 하나다

   새 라우트                     제출도 조회도 이미 있다.  QUEUED 는 그 표면의 값 하나다

   새 타입                       LeaseGrant · Run · QueuedRun 을 그대로 쓴다

   match.go 의 변경               NFR-I4.  busy 는 부르는 쪽이 만들어 넘긴다 (ADR-014 결정 3)

   runctl 의 변경                 client.go:70 이 StatusCode/100 == 2 로 받아 202 가 통과하고,
                                Terminal 이 SUCCEEDED 와 FAILED 뿐이라(client.go:208)
                                --wait 도 QUEUED 에서 계속 폴링한다.  읽고 확인했다

   internal/store/release.go     applyRelease 는 ReportStep 의 트랜잭션 안이라 깨우기를
                                거기 못 둔다.  자리는 커밋 뒤인 postResult 다
                                (functional-design-questions.md 4절)

   GET /v1/runs 의 변경           U-관측API 가 state 필터를 이미 냈다.  이 유닛은 그 값을
                                가진 행이 실제로 생기게 할 뿐이다