The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.4 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | assets | evidence | source | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CONCEPT | cas-tuple-and-update-count | CAS 튜플과 update count가 답이 되는 구조 | owner-safe-state-machines | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | concept:cas-tuple-and-update-count | 2026-09-01 |
|
|
|
CAS 튜플과 update count가 답이 되는 구조
상태를 전이시키는 모든 문장이 소유자 튜플 전체를 where 절에 반복한다. 그래서 갱신 건수가 곧 답이 된다. 한 건이면 이 소유자가 이 리비전에서 여전히 소유자였다는 뜻이고, 0 이면 다른 무언가가 레코드를 움직였다는 뜻이다.
관계
- fenced lease — 만료 시각만으로는 부족한 이유 이 구조가 강제하는 소유권 모델이다.
- CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다 이 개념을 규칙으로 옮긴 것이다.
- native claim이 Version을 올리지 않아 충돌을 보고하지 않는 낙관적 잠금이 됐다 같은 구조가 JPA 버전 컬럼과 만나는 지점의 사례다.
본문
상태 전이를 "읽고 → 판단하고 → PK로 update"하면 그 사이에 takeover한 worker의 상태를 덮어쓴다. 이 저장소의 형태는 소유권 튜플 전체(scope · owner token · attempt · claim operation id · state revision)를 where 절에 반복하고 update count 자체를 답으로 쓰는 것이다.
판정이 되는 갱신 행 수
:::evidence key="cas-tuple-and-update-count-diagram" alt="소유권 튜플 조건부 UPDATE 에서 한 행 갱신과 영 행 갱신 두 갈래가 나온다" caption="판정이 되는 갱신 행 수" zoom="false" :::
1이면 이 owner가 그 revision에서 여전히 owner였고, 0이면 다른 무언가가 record를 움직였으니 caller는 자기 view를 현재로 취급하면 안 된다.
where 절에 반복되는 소유권 튜플
:::evidence key="cas-tuple-and-update-count" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true" :::
polling delivery가 더 얹는 조건
authority EXISTS 서브쿼리를 더해 cutover를 가로지르지 못하게 한다.
:::note
컨테이너 레인 미실행 — 동시 claim에서 실제로 0행이 나오는지 관측하지 않았다
:::
문장이 답을 만든다
/**
* The statements that advance a claim its owner already holds.
*
* <p>Every one repeats the complete owner tuple — scope, token, attempt, claim operation and state
* revision — in its {@code where} clause, so the update count <em>is</em> the answer: one row means
* this owner was still the owner at this revision, zero means something else moved the record and
* the caller must not treat its own view as current.
*/
튜플은 다섯이다. 스코프, 토큰, 시도 번호, 청구 연산, 상태 리비전.
읽고 나서 쓰면 안 되는 이유
/**
* Reading the row and then updating on the scope
* alone would let a worker whose lease expired overwrite the state of the one that took over.
*/
읽기와 쓰기 사이에 다른 작업자가 들어올 수 있다. 스코프만으로 갱신하면 그 사이의 변화를 보지 못한다.
상태 리비전이 함께 오르는 이유
update idempotency_record
set status = 'EXECUTING',
state_revision = state_revision + 1,
last_transition_operation_id = ?,
last_transition_kind = 'START',
last_transition_result_digest = ?,
전이마다 리비전이 오른다. 그래서 같은 소유자라도 자기가 본 리비전이 아니면 갱신이 0 건이 된다. 소유권만으로는 부족하고 시점까지 맞아야 한다.
한 자리에 모으는 이유
/**
* <p>Collected here rather than in the store because they are one family: same guard, same
* interpretation of the count, same reason a caller may not skip the guard. The store decides which
* of them a given outcome permits.
*/
같은 가드와 같은 해석을 공유하는 문장들을 한 타입에 둔다. 저장소는 어떤 결과에 어떤 전이가 허용되는지만 정한다.
이 분리가 하는 일은 가드를 건너뛰는 문장이 새로 생기지 않게 하는 것이다. 문장이 저장소에 흩어져 있으면 하나가 where 절을 짧게 쓰는 것을 막을 방법이 없다.
같은 구조가 다른 곳에도 있다
outbox 폴링 전달 어댑터의 완료 CAS 문장 셋이 같은 형태다. 최종 상태 쓰기가 자기 획득 토큰을 지목하고, 밀려난 작업자의 쓰기는 0 건이 된다.
:::tip
이 구조에서 예외는 실패를 뜻하지 않는다. 갱신 건수 0 은 정상적인 답이고, 그 답을 어떻게 해석할지는 호출자가 정한다. 그래서 경합이 예외 처리 경로가 아니라 정상 경로에 있다.
:::