docs(clean-architecture-backend-template): 제1부가 채택한 것만 글감으로 남기고 다시 고른다
글감 1,001개 중 제1부(§3~§11) 앵커를 하나라도 가진 것은 112개뿐이었다. 나머지 889개는
제2부 모듈 분석 65편의 절 제목에서 나온 것이고, 그것이 재판정이 필요했던 이유다.
주제 44 → 16 (43개가 독자 질문 없이 있었다. 지금은 전부 있다)
글감 1,001 → 123 (제1부 앵커 112 + 제1부가 채택했는데 비어 있던 자리 11)
후보 965 → 1,088 · PENDING 905 → 0
error 3,042 → 0
내려온 889개는 후보 대장에 KEEP_IN_SSOT 로 남는다 — 버린 것이 아니라 분석에 남기고 독립
기록으로 만들지 않기로 한 것이다. 그 글감을 받치던 기록 파일 828개는 지웠다. 계약이 정본이고,
파일이 남아 있다는 이유로 계약에서 뺀 주제가 되살아나면 안 된다. 이력에는 그대로 있다 —
git checkout a0ca2bb -- <경로>.
제1부가 채택했는데 글감이 없던 자리 열하나를 채웠다: mongo high-water mark 가 재전달 이벤트를
삼킨 P1, admin plane 이 가드만 켜고 서비스는 켜지 않은 것과 그 짝인 결정, 실패 어휘 세 층과
SQLState 매트릭스 병합 규칙, 부하 아래에서만 새는 admission 경계, 발행 증거와 완료 판정의
분리, keyset·JSONB 결정 둘.
Concept 17개에 basis-version 을 채우고, 계약 제목과 기록 제목이 갈라져 있던 23건을 기록 쪽에
맞췄다. candidateScope 에 excludedAnchorPattern 을 적어 제2부 앵커만 가진 글감이 다시 올라올
수 없게 한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a0ca2bb72a
commit
1f04117bbf
-116
@@ -1,116 +0,0 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a-digest-that-covered-who-but-not-what
|
||||
title: transition digest가 "누가·언제"만 덮고 "무엇"을 덮지 않아 다른 전이를 같다고 보고했다
|
||||
topic: owner-safe-state-machines
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a-digest-that-covered-who-but-not-what
|
||||
evidenceCapturedOn: 2026-09-02
|
||||
assets:
|
||||
- key: a-digest-that-covered-who-but-not-what
|
||||
file: ../../../final/evidence/rendered/a-digest-that-covered-who-but-not-what.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a-digest-that-covered-who-but-not-what.txt
|
||||
source:
|
||||
- 이 사례의 사실은 분석 문서가 아니라 다이제스트 정책 클래스의 javadoc — 고친 쪽이 남긴 사후 기록 — 과 현재 구현에서 왔다. 후보 원장이 지정한 `final/document.md#a05` 의 절 번호는 그 파일에 존재하지 않는다. 완료 쪽이 아직 열려 있다는 것은 같은 문서 §59.2 이고, 위에 인용한 탐침 값이 그 절에 있다.
|
||||
---
|
||||
|
||||
# transition digest가 "누가·언제"만 덮고 "무엇"을 덮지 않아 다른 전이를 같다고 보고했다
|
||||
|
||||
전이 다이제스트가 전이 종류와 연산 식별자와 소유자와 시도와 상태 리비전을 덮었다. 누가 언제 했는지는 덮고 무엇을 했는지는 덮지 않아서, 결말이 다른 전이가 같은 값을 냈다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **digest는 길이 프레이밍하고 버전을 붙인다**
|
||||
이 사례가 만든 규칙이다.
|
||||
- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다**
|
||||
같은 저장소의 소유자 안전 규칙이다.
|
||||
- **complete()의 replay 판정이 replayTtl 변경을 무시한다**
|
||||
이 다이제스트가 실제 판정에 쓰이지 않는 경로다.
|
||||
|
||||
## 문제
|
||||
|
||||
다이제스트가 덮던 다섯 성분은 전부 누가 몇 번째로 어느 상태에서 했는지를 말한다. 무엇을 했는지를 말하는 성분이 없었다.
|
||||
|
||||
그래서 재시도 가능한 실패와 포기한 실패가 같은 값을 냈고, 응답이 다른 두 완료와 보존 기간이 다른 두 완료도 그랬다.
|
||||
|
||||
## 결론
|
||||
|
||||
다이제스트를 비교한 재생 판정이 세 쌍을 같은 전이로 결론지었다. 세 쌍에서 갈린 것은 호출자의 행동이 달라지는 자리뿐이었다.
|
||||
|
||||
수정은 두 층으로 왔다. 호출부마다 의미 인자를 넘기게 했고, 실패 쪽은 전이 종류 자체를 성향별로 갈랐다.
|
||||
|
||||
이어 붙이는 방식도 바뀌었다. 성분마다 앞에 길이를 적고, 버전 상수를 다이제스트 입력에 넣는다.
|
||||
|
||||
다만 완료 쪽 재생 분기는 이 다이제스트를 비교하지 않는다. 그래서 응답이 같고 재생 창만 다른 두 완료는 지금도 같은 결과로 판정된다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
확인 방식 : 다이제스트 정책과 다섯 호출부, 완료 재생 분기 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 다이제스트 정책의 javadoc 에서 덮던 성분과 충돌한 쌍을 읽는다.
|
||||
2. 전이 다이제스트를 부르는 다섯 지점에서 각각 무엇을 넘기는지 확인한다.
|
||||
3. 실패 쪽 전이 종류가 어떻게 갈라지는지 확인한다.
|
||||
4. 이어 붙이기가 각 성분 앞에 적는 길이와, 해시가 실제로 먹는 바이트를 확인한다.
|
||||
5. 완료 재생 분기가 무엇을 비교하는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
전이 다이제스트가 덮던 것은 전이 종류와 연산 식별자와 소유자 토큰과 시도와 상태 리비전이었다.
|
||||
|
||||
## 결말을 가르는 성분이 하나도 없었다
|
||||
|
||||
:::evidence key="a-digest-that-covered-who-but-not-what" alt="코드베이스에서 전이 다이제스트가 덮던 다섯 성분과 그때 같은 값을 내던 쌍을 적은 javadoc, 지금 다섯 호출부가 실제로 넘기는 의미 인자, 각 성분 앞에 적는 길이와 해시가 먹는 바이트, 버전 상수, 그리고 완료 재생 분기가 비교하는 값을 뽑은 출력 49줄. 완료 재생 분기가 전이 다이제스트가 아니라 연산 식별자와 응답 다이제스트만 본다는 것이 그 출력에 그대로 보인다." caption="덮던 다섯과 충돌한 쌍 · 호출부별 의미 인자 · 길이 프레이밍과 해시 입력 · 완료 재생 분기가 비교하는 값 — 49줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
다섯은 전부 누가 몇 번째로 어느 상태에서 했는지를 말한다. 결말에 가장 가까운 것이 전이 종류인데, 그때는 실패가 성향과 무관하게 하나의 `FAIL` 이었다.
|
||||
|
||||
javadoc 이 같은 값을 내던 쌍 셋을 든다. 재시도 가능한 실패와 포기한 실패, 응답이 다른 두 완료, 보존 기간이 다른 두 완료다.
|
||||
|
||||
세 쌍이 달랐던 부분은 호출자가 실제로 행동을 바꾸는 부분뿐이었다. 다이제스트를 비교한 재생 판정은 그 셋을 전부 같은 전이라고 답했다.
|
||||
|
||||
## 무엇을 넘길지는 호출부가 정한다
|
||||
|
||||
지금은 다섯 호출부가 각자 인자를 넘긴다.
|
||||
|
||||
갱신은 처리 임차 유효 기간을 넘긴다. 완료는 응답 다이제스트와 재생 유효 기간을 넘기고, 그 자리 주석이 이유를 적는다 — 응답 다이제스트를 전이 다이제스트 대신 쓰면 같은 응답을 낸 서로 다른 연산의 완료가 구별되지 않고, 완료가 함께 결정하는 재생 창도 잃는다.
|
||||
|
||||
실패는 성향과 보존 기간을 넘긴다. 그리고 전이 종류 자체가 `FAIL_RETRYABLE` 과 `FAIL_ABANDONED` 로 갈린다. 첫 번째 쌍은 두 겹으로 갈라진 셈이다.
|
||||
|
||||
시작과 해제는 아무것도 넘기지 않는다. javadoc 이 예시로 든 코덱 신원은 지금 어느 호출부도 넘기지 않는다.
|
||||
|
||||
## 이어 붙이는 방식이 구별을 잃게 할 수 있다
|
||||
|
||||
성분은 전부 가변 폭 텍스트다. 그중 소유자 토큰은 문자 집합을 이 플랫폼이 정하지 않는다 — 소유자 타입이 강제하는 것은 1자 이상 128자 이하와 공백 불가뿐이다.
|
||||
|
||||
구분자로 이으면 서로 다른 성분 목록이 하나의 문자열로 렌더링될 수 있다. 그래서 각 성분 앞에 길이를 적고 콜론을 찍은 뒤 값을 적는다.
|
||||
|
||||
적는 길이는 자바 문자 수다. 해시가 먹는 것은 UTF-8 바이트다. 같은 저장소의 메시징 파티션 키는 같은 목적에 UTF-8 바이트 수를 쓰고, 비 ASCII 골든 벡터로 그 선택을 고정한다.
|
||||
|
||||
## 버전 상수는 다이제스트 입력 안에 있다
|
||||
|
||||
상수 값은 2 이고, 이어 붙이기가 그 값을 `v2` 로 맨 앞에 쓴다. 형식이 바뀌면 값이 바뀌므로 옛 형식으로 계산된 다이제스트와 섞이지 않는다.
|
||||
|
||||
저장소 이력에는 이 파일이 지금 형태 그대로 커밋 하나에 들어와 있다. 값이 언제 몇 번 올랐는지는 남아 있지 않다.
|
||||
|
||||
## 완료 쪽은 아직 이 다이제스트를 보지 않는다
|
||||
|
||||
정책이 고쳐진 것과 그 정책이 판정에 쓰이는 것은 다르다.
|
||||
|
||||
이미 완료된 동일 연산의 재생 분기는 전이 다이제스트를 비교하지 않는다. 상태가 COMPLETED 인지, 마지막 전이 종류가 COMPLETE 인지, 연산 식별자가 같은지를 보고, 그다음 응답 다이제스트만 비교한다.
|
||||
|
||||
그래서 응답은 같고 재생 유효 기간만 다른 두 완료가 같은 결과로 판정된다. 분석 문서가 실제 PostgreSQL 탐침으로 그것을 확인했다 — 첫 호출에 한 시간, 두 번째 호출에 아홉 시간을 넘겼는데 두 번째가 같은 결과로 판정됐고, 저장된 창은 3600초 그대로였다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
수정 이전의 충돌을 실행으로 재현하지 않았다. 지금 정책이 지켜지는지는 다이제스트 정책 전용 테스트가 고정한다 — 성향 차이, 응답 차이, 재생 창 차이, 길이 프레이밍, 그리고 널 인자와 인자 없음의 구분이다. 완료 쪽이 아직 열려 있다는 것은 분석 문서의 실측 탐침이 확인했다.
|
||||
|
||||
<!-- body:end -->
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: expired-claim-versus-expired-execution
|
||||
title: 만료된 CLAIMED는 takeover하고 만료된 EXECUTING은 조정을 요구하도록 갈랐다
|
||||
topic: owner-safe-state-machines
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:expired-claim-versus-expired-execution
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: expired-claim-versus-expired-execution
|
||||
file: ../../../final/evidence/rendered/expired-claim-versus-expired-execution.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/expired-claim-versus-expired-execution.txt
|
||||
source:
|
||||
- 원본 분석 절은 final/document.md#a05 §10.1, §10.4 이다.
|
||||
---
|
||||
|
||||
# 만료된 CLAIMED는 takeover하고 만료된 EXECUTING은 조정을 요구하도록 갈랐다
|
||||
|
||||
리스가 만료된 두 상태를 같이 다루면 안 된다. 청구만 하고 실행하지 않은 소유자는 밀어내도 되지만, 실행을 시작한 소유자는 무엇을 했는지 알 수 없으므로 조정으로 넘긴다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **만료된 claim과 만료된 실행은 다르게 다뤄야 한다**
|
||||
이 사례에서 끌어낸 규칙이다.
|
||||
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
|
||||
조정으로 넘기는 판정이 그 규칙의 적용이다.
|
||||
- **fenced lease — 만료 시각만으로는 부족한 이유**
|
||||
리스 만료를 다루는 맥락이다.
|
||||
|
||||
## 문제
|
||||
|
||||
리스가 만료되면 다른 작업자가 그 레코드를 가져갈 수 있어야 한다. 그러지 않으면 죽은 작업자의 레코드가 영원히 막힌다.
|
||||
|
||||
문제는 만료된 소유자가 무엇을 하다가 만료됐는지에 따라 안전한 처리가 다르다는 점이다.
|
||||
|
||||
## 결론
|
||||
|
||||
청구 결정 트리가 두 상태를 갈라 다르게 답한다.
|
||||
|
||||
같은 소유자 토큰이면 연산 충돌로 답한다
|
||||
상태가 CLAIMED 이고 리스가 지났으면 청구를 재설정한다. 즉 가져간다
|
||||
상태가 재시도 가능 실패면 마찬가지로 재설정한다
|
||||
상태가 EXECUTING 이고 리스가 지났으면 복구 필요로 답한다
|
||||
상태가 포기됨이면 복구 필요로 답한다
|
||||
그 외에는 진행 중으로 답하고 재시도 시각을 준다
|
||||
|
||||
CLAIMED 는 자리를 잡았지만 아직 아무것도 실행하지 않은 상태다. 그 소유자를 밀어내도 외부 효과가 없다.
|
||||
|
||||
EXECUTING 은 실행을 시작한 상태다. 그 소유자가 무엇을 어디까지 했는지 이 저장소는 모른다. 밀어내고 다시 실행하면 그 작업이 두 번 일어날 수 있다.
|
||||
|
||||
그래서 EXECUTING 만료는 자동 처리 대상이 아니라 조정 대상이다. 결과 타입이 복구 필요라는 별도 값을 갖고, 그 값이 시도 번호를 함께 들고 간다.
|
||||
|
||||
같은 구별이 해제 경로에도 있다. 소유자 튜플이 다르면 소유자 아님으로 답하고, 상태가 이미 EXECUTING 이면 실행이 시작되었음으로 답하며, CLAIMED 가 아니면 연산 충돌로 답한다. 해제는 아직 실행하지 않은 청구에 대해서만 허용된다.
|
||||
|
||||
같은 형태가 인박스 어댑터에도 있다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
데이터베이스 : PostgreSQL
|
||||
확인 방식 : 결정 트리와 그 javadoc 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 소유자 안전 멱등성 저장소의 청구 결정 트리를 읽는다.
|
||||
2. CLAIMED 만료와 EXECUTING 만료가 각각 어떤 결과를 내는지 확인한다.
|
||||
3. 해제 경로의 상태 검사를 확인한다.
|
||||
4. 인박스 어댑터의 같은 형태를 비교한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
만료된 lease를 일률적으로 takeover하면 이미 실행이 시작된 작업을 blind retry하게 된다.
|
||||
|
||||
## 이 저장소는 상태로 나눈다
|
||||
|
||||
만료된 `CLAIMED`는 `resetClaim`으로 takeover하고, 만료된 `EXECUTING`은 `abandonExpiredExecution`으로 `ABANDONED`에 넣고 `RecoveryRequired`를 반환한다.
|
||||
|
||||
## RecoveryRequired 참조 위치
|
||||
|
||||
:::evidence key="expired-claim-versus-expired-execution" alt="코드베이스에서 RecoveryRequired 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RecoveryRequired 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## inbox도 같은 축을 쓴다
|
||||
|
||||
`RECEIVED`(takeover 가능)와 `PROCESSING`(→ DEAD, recovery-required)로 나눈다. 즉 "claim만 했다"와 "실행에 들어갔다"가 만료 시 다른 결론을 낳는다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
만료된 EXECUTING 이 실제로 조정 큐로 흘러가 사람이 처리하는 경로를 따라가지 않았다. 확인한 것은 저장소가 그 상태를 별도 결과로 답한다는 것이다.
|
||||
|
||||
컨테이너 레인 미실행
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user