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>
12 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 | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a06-f016-flamingock | javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다 | declaration-and-document-drift | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a06-f016-flamingock | 2026-09-02 |
|
|
|
javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다
잠금 어댑터는 펜스로 미지정 값을 돌려주고 그 근거를 javadoc 에 적는다. 이어지는 한 문장이 실행자의 거부 범위를 재개 가능한 마이그레이션으로 한정한다. 실제 조건은 리스의 펜스 값 하나여서 마이그레이션이 하나도 없어도 거부한다.
관계
- fenced lease — 만료 시각만으로는 부족한 이유 펜싱이 무엇을 보장하는지 다룬 개념이다.
- recordApplied가 펜스를 비교하지 않아 밀려난 실행자가 원장을 차지한다 같은 원장의 펜스 처리 사례다.
- 문서가 UUIDv7이라 말하고 생성되는 것은 v4다 두 사례 모두 javadoc 이 적은 것과 코드가 만드는 값이 다르다.
문제
펜스 메서드가 미지정 값을 돌려주는 것과 그 근거는 옳다. 로컬 카운터로 펜싱을 흉내내면 펜싱처럼 보이면서 아무것도 보호하지 않고, 프로세스마다 따로 세는 수는 다른 프로세스가 무엇을 획득했는지에 대해 아무 말도 하지 않는다.
어긋난 것은 같은 javadoc 의 마지막 한 문장이다. 실행자가 바로 이 이유로 재개 가능한 마이그레이션을 거부한다고 적는다.
결론
그 문장이 가리키는 장치는 실재한다. 재개 가능한 마이그레이션만 체크포인트를 남기고, 그 저장에 펜스가 넘어가며, 원장은 미지정 펜스로 들어온 체크포인트를 거절한다. 그 자리를 지키는 조건은 재개 가능성 하나만 본다.
닿지 못할 뿐이다. 진입점의 세 관문 중 세 번째가 리스의 펜스 값만 보고 던지고, 그 자리는 적용 스트림보다 앞이다. 조건에 마이그레이션이 들어가지 않으므로 목록이 비어 있어도 같은 문장이 나온다.
탐침으로 확인했다. 빈 목록을 이 리스로 적용해도 거절된다. 목록에 아무것도 없어 재개 가능한지 여부가 존재하지 않는데도 같다. 체크포인트를 만들지 않는 마이그레이션 한 건도 마찬가지이고, 그 한 건은 펜스가 있는 리스에서는 완료로 끝난다.
거절 문장 자체는 정확하다. 펜스 없는 리스는 멈춘 실행자를 배제할 수 없다고 말하고 재개 가능성을 언급하지 않는다. 그리고 거절 자체가 안전한 선택이다. 문제는 javadoc 을 읽고 이 어댑터를 고른 쪽이 재개 불가능한 마이그레이션은 돈다고 읽는다는 것이다.
수정은 사실상 하나다. javadoc 을 실제 범위로 고치는 것이다. 검사를 스트림 안으로 옮기는 쪽은 둘에 막힌다. 재개 가능성은 실행이 체크포인트를 돌려준 뒤에야 드러나므로 실행 전에 판별할 수 없고, 옮기더라도 적용이 끝난 뒤 같은 미지정 값이 원장 기록으로 넘어가 거기서 거절된다. 거절 시점이 실행 전에서 실행 후로 밀릴 뿐이다.
지금 배포를 멈추는 결함은 아니다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 이 어댑터를 만드는 곳은 시험 두 곳이며 그 시험들은 적용을 부르지 않는다. 이 리프를 가져다 조립하는 쪽이 처음 실행할 때 드러난다.
검증 환경
OpenJDK : 21.0.12 확인 방식 : 밀폐된 탐침으로 실행자 진입점 호출 소스 수정 : x
재현 조건
- 잠금 어댑터의 펜스 메서드가 돌려주는 값과 그 javadoc 을 읽는다.
- 진입점이 지나는 세 관문과 적용 스트림의 줄 번호를 확인한다.
- 그 파일에서 리스의 펜스를 읽는 곳을 전수로 세고, 각각이 그 값을 어디로 넘기는지 확인한다.
- 원장이 미지정 펜스를 어떻게 다루는지 읽는다.
- 마이그레이션 인터페이스에 재개 가능성을 선언하는 멤버가 있는지 확인한다.
- 어댑터로 빈 목록을 적용한다.
- 체크포인트를 만들지 않는 마이그레이션 한 건을 같은 리스로, 그리고 펜스가 있는 리스로 각각 적용한다.
- 어댑터가 나오는 곳과 프로덕션 참조를 전수로 센다.
본문
잠금 어댑터의 펜스 메서드는 미지정 값을 돌려주고, javadoc 이 그 선택의 근거를 적는다.
근거 문단과 그 뒤의 한 문장
:::evidence key="a06-f016-flamingock" alt="잠금 어댑터의 펜스 메서드와 그 javadoc, 실행자 진입점이 지나는 세 관문, 그 파일에서 리스의 펜스를 읽는 곳 전수와 각각의 줄 번호, 원장이 미지정 펜스를 거절하는 자리, 마이그레이션 인터페이스의 멤버 목록, 이 어댑터가 나오는 곳 전수, 실행자와 원장과 잠금의 프로덕션 참조, 그리고 마이그레이션을 적용하는 진입점 전수를 출력한 터미널 기록." caption="펜스는 UNFENCED 를 반환하고 javadoc 은 재개 가능한 것을 거부한다고 적음 · 관문은 보유 검사 74행, 목록 검증 80행, 펜스 검사 82행이고 적용 스트림은 93행 · 펜스를 읽는 곳은 82·154·157행이고 뒤 둘은 원장으로 넘어가 미지정이면 거절 · 인터페이스에 재개 가능성 멤버 없음 · 프로덕션 참조 0 — 73줄 · exit 0" zoom="true" :::
/**
* Unfenced: the engine's lock exposes no monotonic token.
*
* <p>Reported honestly rather than synthesised from a local counter, which would look like
* fencing and protect nothing — a per-process counter says nothing about what another process
* acquired. The runner refuses resumable migrations under an unfenced lease for exactly this
* reason.
*/
앞 두 문장은 왜 흉내내지 않았는지를 적는다. 문제는 마지막 문장이 거절 범위를 재개 가능한 마이그레이션으로 한정한 것이다.
그 문장이 가리키는 장치는 실재한다
실행자가 리스의 펜스를 읽는 곳은 셋이다.
82: if (lock.fence() == MongoMigrationLock.UNFENCED) {
154: result.resumePoint().ifPresent(checkpoint -> ledger.saveCheckpoint(checkpoint, lock.fence()));
157: migration.id(), migration.checksum(), context.operator(), clock.instant(), lock.fence());
154행은 재개 지점을 남긴 마이그레이션에만 걸린다. 그 값을 받은 원장은 미지정이면 거절한다.
private static void requireCurrentFence(long fence, String what) {
if (fence == MongoMigrationLock.UNFENCED) {
재개 가능성에만 반응하는 펜스 조건이 그 자리에 있다. javadoc 의 마지막 문장은 근거 없는 착오가 아니다.
세 번째 관문이 먼저 던진다
74: if (!lock.held()) {
80: migrations.forEach(this::validate);
82: if (lock.fence() == MongoMigrationLock.UNFENCED) {
93: return migrations.stream()
82행의 조건은 리스의 펜스 값 하나다. 80행은 목록을 순회하지만 원장과 체크섬을 볼 뿐이고, 82행은 그 결과도 마이그레이션도 참조하지 않는다. 154행에 닿으려면 93행의 스트림에 들어가야 하는데 82행이 그보다 앞이다.
그 자리에서 나오는 문장도 재개 가능성을 말하지 않는다.
"this migration lease exposes no fencing token, so a stalled runner cannot be excluded;"
+ " use a fenced lease or run the migration inside a maintenance window with the"
+ " application stopped"
빈 목록도 거절된다
:::evidence key="a06-f016-flamingock-probe" alt="잠금 어댑터가 보고하는 보유 여부와 펜스 값, 빈 마이그레이션 목록과 체크포인트를 만들지 않는 마이그레이션 한 건을 그 리스로 적용한 결과와 거절 문장, 그리고 같은 마이그레이션을 펜스가 있는 리스로 적용한 결과를 출력한 터미널 기록." caption="펜스는 -1 · 빈 목록도 거절, 재개 불가 한 건도 같은 문장으로 거절 · 같은 마이그레이션이 펜스가 있는 리스에서는 COMPLETED — 11줄 · exit 0" zoom="true" :::
이 어댑터로 리스를 만들고 아무것도 없는 목록을 적용했다.
[검사가 마이그레이션을 보기 전에 걸린다]
마이그레이션 0건 -> 거절, MongoOperationRejectedException
this migration lease exposes no fencing token, so a stalled runner cannot be excluded; ...
재개 불가 1건 -> 거절, MongoOperationRejectedException
this migration lease exposes no fencing token, so a stalled runner cannot be excluded; ...
목록이 비어 있으면 재개 가능한지 여부가 존재하지 않는데 같은 문장이 나온다. 한 건을 넣어도 결과는 같다. 그 한 건은 한 번의 호출로 완료를 돌려주므로 체크포인트를 남기지 않는다.
같은 마이그레이션을 펜스가 있는 리스로 적용하면 이렇게 된다.
[같은 마이그레이션, 펜스가 있는 리스]
재개 불가 1건 -> 실행, 결과 1건 COMPLETED
거절의 대상은 마이그레이션이 아니라 리스다.
검사를 옮기는 수정은 두 곳에 막힌다
마이그레이션 인터페이스에는 재개 가능성을 선언하는 멤버가 없다.
17: MongoMigrationId id();
20: MongoMigrationChecksum checksum();
23: MongoMigrationPrecondition precondition();
26: MongoMigrationResult execute(MongoMigrationContext context);
29: MongoMigrationPostcondition postcondition();
재개 가능한지는 execute 가 체크포인트를 돌려준 뒤에야 드러난다. 그리고 82행을 스트림 안으로 옮겨도 157행이 같은 미지정 값을 원장 기록으로 넘긴다. 원장은 그것을 거절하므로, 데이터베이스는 바뀌고 원장에는 아무것도 남지 않는 상태가 된다.
이 어댑터가 나오는 곳
어댑터를 만드는 곳은 시험 두 곳이다. 하나는 획득 실패를, 다른 하나는 해제와 해제 뒤 갱신 거절을 확인한다. 적용을 부르는 시험은 없어서 javadoc 과 82행의 어긋남이 시험에도 걸리지 않는다.
조립 쪽도 같다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 자동 구성이 이 패키지에서 만드는 빈도 없다. 지금 도는 배포를 멈추는 결함이 아니라, 이 리프를 가져다 조립하는 쪽이 처음 실행하는 순간에 드러날 결함이다.
확인하지 못한 것
이 어댑터를 실제 배포에서 골랐을 때의 운영 경험은 다루지 않았다. 확인한 것은 세 번째 관문의 조건과 그 조건이 만드는 거절의 범위다.