문서와 심볼

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

U-대기열 Functional Design — 계약 질문 넷

이 유닛은 기존의 뜨거운 경로 둘을 고친다 — 모든 제출이 지나는 submit 과
모든 단계 보고가 지나는 postResult 다. 아래 넷은 그 둘의 모양을 가르는데
어느 문서도 값을 안 준 자리다. 나머지는 2절에 근거를 적고 닫았다.

계획 파일을 안 만들었다. 규칙 Step 4 는
construction/plans/{unit}-functional-design-plan.md 를 요구하지만 사용자
지시가 「계획 파일은 Units Generation 과 Code Generation 에서만 만든다」이다.
이 단계의 계획은 3절 하나로 족하다 — U-바닥에서와 같은 처리다.

딛는 것은 U-바닥이 이미 냈다. store.StateQueued · store.ListQueued ·
store.DrainingNodes 가 internal/store/queue.go 에 있고, ValidState 가
QUEUED 를 이미 받는다 (internal/store/list.go:38). 이 유닛이 하는 것은
그 어휘를 가진 행을 실제로 만들고 다시 움직이는 것이다.


1. 질문 넷

권장을 A 로 뒀다. [Answer]: 를 채워 주면 그 값으로 산출물 셋을 낸다.

Q1. 승격이 어느 층에 사는가

requirements.md §4.1 이 store.WakeQueued(ctx) error 로 시그니처를
고정했다.
고정되지 않은 것은 그 안이 얼마나 하느냐다 — 매칭 조합이 store
에 사는가, api 에 남는가.

A. store.WakeQueued 가 승격을 통째로 진다. (권장)

   store.WakeQueued(ctx)
       ListQueued                       created_at 오름차순
       LiveAdverts + 못 쓰는 노드         후보와 제외 목록을 한 번 읽는다
       Run 하나마다 독립 트랜잭션
           match.Match  ->  폭 상한  ->  임대 발급  ->  runs.state = RUNNING
           assigned 채우기  ->  steps.node_id 채우기
           runAcquires  ->  raiseAsks
       실패하면 로그를 남기고 다음 Run 으로

   internal/api/queue.go     깨우기 호출 · 로그 · 202 응답 조립만 진다

label 을 internal/api/api.go:849 에서 internal/store 로 옮긴다 — 승격도
제출도 []match.Assignment 를 []store.Assigned 로 옮겨야 하고, 두 벌이
되면 Record 의 노드 이름이 경로마다 갈린다.

B. 조합을 internal/api 가 계속 쥔다. Store 에 승격 함수를 필드로 주입한다
(AnswerPath 와 같은 방식) 하고 WakeQueued 는 목록을 돌며 그것을 부른다.

A 인 이유 셋:

   Reap 이 깨우기 지점이다        그 자리는 store 안이고 api 를 모른다.  A 면 s.WakeQueued(ctx)
                                 한 줄이다.  B 면 store 가 자기 필드로 나갔다 오는 왕복이 되고,
                                 필드를 안 배선한 테스트에서 조용히 아무것도 안 한다

   이름과 소유가 갈리지 않는다     B 의 WakeQueued 는 이름만 store 의 것이다

   매처는 그대로 순수하다          패키지 주석의 「매칭 로직은 여기 없다」가 뜻하는 것은
                                 ADR-014 결정 3 의 순수 함수다.  A 는 match.Match 를 부르기만
                                 하고 안 고친다.  store 는 이미 internal/match 를 임포트한다
                                 (Run.Reject 가 그 타입이다)

A 의 값은 조합이 두 자리에 생긴다는 것이다. 겹치는 입력만 store 메서드
하나로 빼고 나머지는 안 합친다 — 근거는 2절 ⑤ 다.

Q2. 깨우기가 동기인가 비동기인가

깨우기 지점이 뜨거운 경로에 붙는다. postResult 는 모든 단계 보고가 지난다.

A. 동기다. 그 자리에서 WakeQueued 를 부르고, 오류는 로그만 남긴다.
응답 코드도 본문도 안 바뀐다. (권장)

B. 고루틴으로 띄운다.

A 인 이유:

   게이트가 그것을 요구한다     CP2 의 「a 가 끝난 뒤 runctl status <b> -> RUNNING」은
                             B 에서 경쟁이다.  간헐적으로 빨간 게이트는 게이트가 아니다

   이미 무거운 것이 동기다      postResult 는 SettleIfDone 을 동기로 부른다 — 판정 · 함대
                             조회 · 봉인 · 파일 쓰기가 그 안에 있다.  WakeQueued 는 그보다 가볍고,
                             대기열이 비면 질의 하나다

   ctx 가 문제다              B 는 r.Context() 를 쓸 수 없다 — 응답이 끝나면 취소되므로
                             승격이 중간에 끊긴다.  context.Background() 로 피하면
                             main 의 종료 순서에 기다릴 것이 하나 더 붙는다

Q3. 어느 409 가 QUEUED 가 되는가

제출 경로에서 409 를 내는 자리가 둘이다.

   internal/api/api.go:419    match.Match 의 Reject{Code: CodeAllBusy}
                              후보는 있는데 전부 점유됨

   internal/api/api.go:474    CreateRun 의 ErrNodeTaken
                              "another run took the node during allocation"
                              배정 도중 다른 Run 이 노드를 가져갔다 (I5 롤백)

A. 둘 다 QUEUED 다. 제출 경로에서 409 가 사라진다. (권장)

B. 매처의 409 만 QUEUED 다. 경쟁 패배는 그대로 409 를 낸다.

A 인 이유 — 완료 조건이 「409 가 QUEUED 가 된다」이고 B 면 그 문장이
절반만 참이다. 호출자는 두 409 를 구별할 수단이 없다: 둘 다 「지금은 자리가
없다」이고 둘 다 다시 내면 되는 것이다. 대기열을 넣은 이유가 그 재시도를
호출자에게서 걷어오는 것인데, B 는 경쟁에서 진 제출만 호출자에게 남긴다.
ErrNodeTaken 은 트랜잭션이 통째로 롤백되므로 손으로 풀 것이 없고, 그 자리에서
QUEUED 로 다시 만드는 것이 안전하다.

A 에서도 폭 상한 거절은 그대로 FAILED 다 — 그것은 CodeNoCandidate
(422) 이고 「다시 내도 같다」이기 때문이다.

Q4. Reap 의 어느 자리에서 깨우는가

완료 조건에 두 문장이 함께 있다 — 「깨우기가 다섯 지점에 있다 (Reap 포함)」과
「reaper 주기에 안 얹었다」다. Reap 은 RunReaper 가 RenewSeconds
마다 부른다 (cmd/mediator/main.go:87).

A. Reap 이 실제로 무언가를 놓았을 때만 깨운다. (권장)
되묻기 만료로 정산된 Run 이 있거나 만료 임대 회수가 행을 건드렸으면 Reap
끝에서 한 번. 아무것도 안 놓은 스캔은 대기열을 안 본다.

B. Reap 끝에서 무조건 한 번.

A 인 이유 — B 는 회수할 것이 없는 스캔마다 ListQueued 를 돌리므로
사실상 주기 폴링이고, 완료 조건의 둘째 문장과 어긋난다. A 는 「자원이
풀렸다」는 사건에 붙는다 — 나머지 지점 넷과 같은 성질이다.


2. 묻지 않고 닫은 것 열

질문 후보였다가 문서와 코드 대조로 닫힌 것이다.

① 202 의 본문은 runView 다. State 가 QUEUED · Steps 전부
PENDING 이고 node 가 빈 값 · Assigned 없음 · Reject 없음 · Warnings
는 201 과 같게 싣는다. requirements.md §3.1 이 GET /v1/runs/{id} 의
모양을 그렇게 고정했고, 제출 응답과 조회 응답이 다르면 읽는 쪽이 둘을 안다.

② 임대 TTL 은 Store 필드로 받는다. §4.1 이 WakeQueued(ctx) error 로
인자 없는 모양을 고정했으므로 승격이 쓸 not_after 를 인자로 못 받는다.
MaxLeasesPerRun 이 이미 같은 방식으로 배선돼 있다 (cmd/mediator/main.go
의 openRecords) — 새 방식을 만들지 않고 LeaseTTL time.Duration 을 그 옆에
둔다.

③ Records.Open 은 제출 시점이다. §3.1 이 못 박았다. Cancel 이
sealRecord 를 부르므로 (internal/store/reap.go:207) 대기 중 취소가 봉인까지
닿으려면 그때 이미 열려 있어야 한다.

④ 대기 중 취소는 코드를 안 고쳐도 된다. Cancel 이
state NOT IN ('SUCCEEDED','FAILED') 로 거르므로 QUEUED 가 그대로 걸리고,
steps 를 PENDING 에서 FAILED 로 옮기고, 없는 임대를 지우고, 봉인한다.
읽고 확인했다.

⑤ submit 과 WakeQueued 를 한 함수로 안 합친다 — 입력만 합친다.
겹치는 것은 「후보 목록과 못 쓰는 노드를 읽는 것」이고 그것만 store 메서드
하나로 뺀다 (BusyNodes 와 DrainingNodes 의 합집합. FR-11 의 중앙 쪽이다).
나머지는 다르다 — submit 은 새 행을 만들고 HTTP 갈래 여섯을 가르고 dry-run
을 겸하며, WakeQueued 는 있는 행을 옮긴다. 합치면 인자 플래그가 셋 붙는다.

⑥ dry-run 은 안 바뀐다 (NFR-I6). 지금 dry-run 은 busy 를 아예 안 읽는다
(internal/api/api.go:402 의 if !dry). draining 을 거기 합치면 응답이
바뀌므로 안 합친다. 합치는 것은 dry == false 갈래 하나다.

⑦ 승격 시 폭 상한을 다시 검사하되, 넘으면 그 Run 을 실패시키지 않는다.
t=0 의 422 가 영구인 이유는 「다시 내도 같다」인데, 승격 시점의 초과는 그때
고른 노드 조합의 문제라 다음 깨우기에 달라질 수 있다 — 한 노드가 두 역할을
맡으면 셈이 줄기 때문이다 (api.go 의 seen 이 그 중복을 접는다). 로그에
남기고 QUEUED 로 둔다.

⑧ QUEUED Run 의 steps.node_id 는 빈 문자열이다. §3.1 이 정했고,
ClaimStep 이 WHERE s.node_id = $1 로 자기 몫만 보므로 빈 값인 단계는 아무
노드도 못 집는다. 승격이 그 열을 채우는 것이 곧 배분이다 —
internal/store/claim.go 의 「당기기가 이미 배분이다」가 그대로 성립한다.

⑨ work_id 는 제출 시점에 박는다. CreateRun 이 하는 그대로다
(internal/store/store.go:323). Run 을 넘어 사는 유일한 식별자라 대기 중에도
있어야 현황판이 같은 Work 의 Run 둘을 잇는다.

⑩ GET /v1/runs 도 runctl 도 안 고친다. 목록 쪽은 U-관측API 가
state 필터를 이미 냈고 QUEUED 는 그 어휘의 값 하나다. runctl 쪽은 읽고
확인했다 — internal/runctl/client.go:70 이 StatusCode/100 == 2 로 받아
202 가 통과하고, Terminal 이 SUCCEEDED 와 FAILED 뿐이라 (client.go:208)
--wait 도 QUEUED 에서 계속 폴링한다.


3. 답을 받으면 낼 것

   [ ]  Q1 ~ Q4 의 답을 받는다
   [ ]  답에 모호한 것이 있으면 후속 질문을 낸다.  없으면 그대로 간다
   [ ]  domain-entities.md      QUEUED Run 의 행 모양 · 새 Store 필드 · 옮기는 함수
   [ ]  business-logic-model.md 제출 갈래 · 승격 절차 · 깨우기 자리와 그 근거
   [ ]  business-rules.md       규칙 · 완료 조건 · 테스트 목록 · CP2

frontend-components.md 를 안 낸다. 이 유닛에 화면이 없다 — S1 의
QUEUED 칩과 행은 U-관측API 가 이미 그렸고 지금 빈 목록을 낸다. 이 유닛이
하는 것은 그 목록에 행이 생기게 하는 것이다.


4. 코드를 읽고 찾은 정정 하나

「부분 반납 뒤 깨우기」는 독립된 호출 자리가 아니다.

unit-of-work.md 의 U-대기열 파일 행렬이 internal/store/release.go 를
「부분 반납 뒤 깨우기」로 적었다. 그 자리에서는 깨울 수 없다.

   ReportStep          tx, err := s.pool.Begin(ctx)          claim.go:773
     afterStep           applyStepEffects                    claim.go:843
       applyStepEffects    applyRelease                      claim.go:863
         applyRelease        DELETE FROM leases   (tx 안)     release.go:73
   ReportStep          tx.Commit(ctx)                        claim.go:823

applyRelease 는 ReportStep 의 트랜잭션 안에서 돈다. 거기서 깨우면
아직 커밋 안 된 임대 해제를 보고 승격하려 들고, 승격의 트랜잭션이 같은 행을
기다린다. 자리는 커밋 뒤 — 즉 postResult 다.

그래서 postResult 의 깨우기 한 번이 「완료」와 「부분 반납」 둘을 덮는다.
딸린 조건이 하나 붙는다: SettleIfDone 이 빈 문자열을 내도(아직 진행 중이라는
뜻이다) 깨워야 한다. 안 그러면 부분 반납으로 풀린 노드를 아무도 안 본다.

파급 — internal/store/release.go 를 안 고친다. 이 유닛이 만지는 파일이
여섯에서 다섯으로 준다. 깨우기 지점은 여전히 여섯이고 (requirements.md
FR-9), 호출 자리가 다섯인 것뿐이다.

   postResult          완료 · 부분 반납        api.go
   postCancel          취소                    api.go
   Reap                회수 · 되묻기 만료       store/reap.go
   postNodes           노드 재기동 감지         api.go
   postNodes           drain 해제 감지          api.go       U-drain 이 단다
   run()               기동 시 한 번            cmd/mediator/main.go