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.9 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | body | assets | evidence | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | two-owners-popped-the-evidence-frame | 커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다 | commit-ambiguity-as-a-result | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:two-owners-popped-the-evidence-frame | 2026-09-01 | case-two-owners-popped-the-evidence-frame.body.md |
|
|
|
커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다
증거 프레임을 pop 하는 주인이 둘이었다. REQUIRES_NEW 안쪽 트랜잭션에서 executor 가 바깥 프레임을 자기 것으로 오인해 pop 했고, 이후 바깥의 커밋 실패는 operation 도 조정 키도 없이 보고됐다.
관계
- 모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다 증거가 사라지면 세 번째 결과를 만들 재료도 사라진다.
- 재시도 단위는 statement가 아니라 유스케이스 전체다 executor 의 finally 가 프레임을 정리하려 한 이유가 그 재시도 단위에 있다.
- 커밋 증거 단계 — NOT_STARTED에서 UNKNOWN까지 이 프레임이 담는 단계 모델이다.
문제
증거 컨텍스트는 스레드에 묶인 프레임 스택이다. 스택인 이유는 REQUIRES_NEW 가 같은 스레드에서 바깥 트랜잭션을 suspend 하고 안쪽을 시작하기 때문이다. 슬롯이 하나면 안쪽의 커밋이 바깥의 phase 를 덮어쓰고, 나중에 바깥에서 난 커밋 실패가 이미 끝난 작업의 증거로 분류된다.
문제는 그 프레임을 pop 하는 주인이 둘이었다는 점이다.
트랜잭션 매니저 : commit 과 rollback 에서 자기 프레임을 정리 executor 의 finally : operation 과 attempt 가 일치하면 top 프레임을 정리
바깥과 안쪽이 같은 operation 이고 같은 attempt 인 경우, executor 의 조건은 안쪽 프레임만큼이나 바깥 프레임도 정확하게 서술한다. 그리고 기본 경로에서는 둘 다 attempt 1 이므로 이 조건이 항상 성립한다.
순서는 이렇게 된다. 안쪽 매니저가 안쪽 프레임을 pop 하고, executor 가 바깥 프레임을 자기 것으로 오인해 pop 한다. 이후 바깥 트랜잭션의 커밋 실패는 operation 없이, attempt 1 로, 조정 키 없이 보고된다.
결론
소유권을 내용이 아니라 깊이로 식별하도록 바꿨다.
scope 가 스택에서의 자기 위치를 들고, 닫을 때 그 위치가 top 일 때만 pop 한다. 매니저는 phase 만 표시하고 아무것도 pop 하지 않는다.
깊이를 기준으로 삼은 이유는 내용이 변하기 때문이다. phase 전이마다 프레임 값이 교체되지만 깊이는 변하지 않고, 엄격하게 중첩된 수명주기에서는 이 scope 가 push 한 깊이의 프레임이 곧 이 scope 의 프레임이다.
순서를 어겨 닫으면 아무것도 pop 하지 않는다. 이것은 의도된 동작이다. 중첩되지 않은 수명주기는 버그이고, 무관한 프레임을 지워서 그것을 덮는 것이 원래 결함이 밖에서 보이던 모습이기 때문이다.
프레임이 남는 것은 프레임이 없는 것보다 나쁘다. 풀링된 요청 스레드가 낡은 COMMITTING 을 무관한 작업으로 들고 가고, 플랫폼은 존재한 적 없는 트랜잭션에 대해 completion-unknown 을 보고하게 된다.
ThreadLocal 을 withInitial 로 만들지 않은 것도 같은 계열의 판단이다. 초기화하는 스레드 로컬은 읽을 때마다 값을 설치하므로, 마지막 프레임을 지운 뒤의 읽기가 clear 가 방금 제거한 것을 다시 등록한다.
검증 환경
OpenJDK : 21.0.12 Gradle : 9.0.0 Spring Boot : 4.0.8 근거 : 저장소의 javadoc 이 사후 기록으로 남긴 회귀와 현재 코드 형태
재현 조건
수정된 형태를 확인하는 절차는 다음과 같다.
- TransactionEvidenceContext 의 클래스 javadoc 에서 두 주인 문제의 서술을 읽는다.
- TransactionEvidenceScope 가 depth 를 들고 ownsTopOf 로 판정하는지 확인한다.
- close 가 멱등인지 확인한다. 두 번째 close 는 그 사이에 들어온 것을 pop 하지 않고 아무것도 하지 않는다.
- TransactionEvidenceScopeTest 의 hasRawThreadLocalValue 단언을 확인한다. 프레임이 남지 않았음을 스레드 로컬 수준에서 검사한다.
본문
증거 프레임을 pop하는 주인이 둘이었다 — 트랜잭션 매니저가 commit/rollback에서, executor의 finally가 operation·attempt 일치 시.
두 주인이 pop 하는 조건
:::evidence key="two-owners-popped-the-evidence-frame" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true" :::
기본 경로에서는 항상 겹친다
바깥과 같은 operation·같은 attempt를 가진 REQUIRES_NEW 안쪽 트랜잭션에서 — 기본 경로에서는 둘 다 attempt 1이라 항상 그렇다 — 안쪽 매니저가 안쪽을 pop하고 executor가 바깥을 자기 것으로 오인해 pop했다. 이후 바깥의 커밋 실패는 operation 없이, attempt 1로, reconciliation key 없이 보고됐다.
해결
깊이로 소유권을 식별하고 자기 프레임이 top일 때만 pop한다.
확인하지 못한 것
없다. 수정된 형태와 그 테스트를 코드로 확인했다.