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>
187 lines
12 KiB
Markdown
187 lines
12 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a06-f016-flamingock
|
|
title: javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다
|
|
topic: declaration-and-document-drift
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a06-f016-flamingock
|
|
evidenceCapturedOn: 2026-09-02
|
|
assets:
|
|
- key: a06-f016-flamingock
|
|
file: ../../../final/evidence/rendered/a06-f016-flamingock.svg
|
|
- key: a06-f016-flamingock-probe
|
|
file: ../../../final/evidence/rendered/a06-f016-flamingock-probe.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a06-f016-flamingock.txt
|
|
- ../../../final/evidence/raw/a06-f016-flamingock-probe.txt
|
|
source:
|
|
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L942 이다. 등급은 P3 이다. 그 절이 담은 것은 넷이다. 펜스 반환값과 그 근거, javadoc 마지막 문장의 범위, 82행 검사가 적용 스트림보다 앞이라는 위치, 그리고 시험이 이 조합을 다루지 않는다는 것.
|
|
- 빈 목록도 같은 문장으로 거절된다는 것, 같은 마이그레이션이 펜스가 있는 리스에서는 완료된다는 것, 154행이 재개 가능한 마이그레이션에만 걸리는 펜스 조건이라는 것, 그리고 검사를 옮겨도 157행에서 다시 막힌다는 것은 이 기록에서 확인했다.
|
|
---
|
|
|
|
# javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다
|
|
|
|
잠금 어댑터는 펜스로 미지정 값을 돌려주고 그 근거를 javadoc 에 적는다. 이어지는 한 문장이 실행자의 거부 범위를 재개 가능한 마이그레이션으로 한정한다. 실제 조건은 리스의 펜스 값 하나여서 마이그레이션이 하나도 없어도 거부한다.
|
|
|
|
## 관계
|
|
|
|
- **fenced lease — 만료 시각만으로는 부족한 이유**
|
|
펜싱이 무엇을 보장하는지 다룬 개념이다.
|
|
- **recordApplied가 펜스를 비교하지 않아 밀려난 실행자가 원장을 차지한다**
|
|
같은 원장의 펜스 처리 사례다.
|
|
- **문서가 UUIDv7이라 말하고 생성되는 것은 v4다**
|
|
두 사례 모두 javadoc 이 적은 것과 코드가 만드는 값이 다르다.
|
|
|
|
## 문제
|
|
|
|
펜스 메서드가 미지정 값을 돌려주는 것과 그 근거는 옳다. 로컬 카운터로 펜싱을 흉내내면 펜싱처럼 보이면서 아무것도 보호하지 않고, 프로세스마다 따로 세는 수는 다른 프로세스가 무엇을 획득했는지에 대해 아무 말도 하지 않는다.
|
|
|
|
어긋난 것은 같은 javadoc 의 마지막 한 문장이다. 실행자가 바로 이 이유로 재개 가능한 마이그레이션을 거부한다고 적는다.
|
|
|
|
## 결론
|
|
|
|
그 문장이 가리키는 장치는 실재한다. 재개 가능한 마이그레이션만 체크포인트를 남기고, 그 저장에 펜스가 넘어가며, 원장은 미지정 펜스로 들어온 체크포인트를 거절한다. 그 자리를 지키는 조건은 재개 가능성 하나만 본다.
|
|
|
|
닿지 못할 뿐이다. 진입점의 세 관문 중 세 번째가 리스의 펜스 값만 보고 던지고, 그 자리는 적용 스트림보다 앞이다. 조건에 마이그레이션이 들어가지 않으므로 목록이 비어 있어도 같은 문장이 나온다.
|
|
|
|
탐침으로 확인했다. 빈 목록을 이 리스로 적용해도 거절된다. 목록에 아무것도 없어 재개 가능한지 여부가 존재하지 않는데도 같다. 체크포인트를 만들지 않는 마이그레이션 한 건도 마찬가지이고, 그 한 건은 펜스가 있는 리스에서는 완료로 끝난다.
|
|
|
|
거절 문장 자체는 정확하다. 펜스 없는 리스는 멈춘 실행자를 배제할 수 없다고 말하고 재개 가능성을 언급하지 않는다. 그리고 거절 자체가 안전한 선택이다. 문제는 javadoc 을 읽고 이 어댑터를 고른 쪽이 재개 불가능한 마이그레이션은 돈다고 읽는다는 것이다.
|
|
|
|
수정은 사실상 하나다. javadoc 을 실제 범위로 고치는 것이다. 검사를 스트림 안으로 옮기는 쪽은 둘에 막힌다. 재개 가능성은 실행이 체크포인트를 돌려준 뒤에야 드러나므로 실행 전에 판별할 수 없고, 옮기더라도 적용이 끝난 뒤 같은 미지정 값이 원장 기록으로 넘어가 거기서 거절된다. 거절 시점이 실행 전에서 실행 후로 밀릴 뿐이다.
|
|
|
|
지금 배포를 멈추는 결함은 아니다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 이 어댑터를 만드는 곳은 시험 두 곳이며 그 시험들은 적용을 부르지 않는다. 이 리프를 가져다 조립하는 쪽이 처음 실행할 때 드러난다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
확인 방식 : 밀폐된 탐침으로 실행자 진입점 호출
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 잠금 어댑터의 펜스 메서드가 돌려주는 값과 그 javadoc 을 읽는다.
|
|
2. 진입점이 지나는 세 관문과 적용 스트림의 줄 번호를 확인한다.
|
|
3. 그 파일에서 리스의 펜스를 읽는 곳을 전수로 세고, 각각이 그 값을 어디로 넘기는지 확인한다.
|
|
4. 원장이 미지정 펜스를 어떻게 다루는지 읽는다.
|
|
5. 마이그레이션 인터페이스에 재개 가능성을 선언하는 멤버가 있는지 확인한다.
|
|
6. 어댑터로 빈 목록을 적용한다.
|
|
7. 체크포인트를 만들지 않는 마이그레이션 한 건을 같은 리스로, 그리고 펜스가 있는 리스로 각각 적용한다.
|
|
8. 어댑터가 나오는 곳과 프로덕션 참조를 전수로 센다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
잠금 어댑터의 펜스 메서드는 미지정 값을 돌려주고, javadoc 이 그 선택의 근거를 적는다.
|
|
|
|
## 근거 문단과 그 뒤의 한 문장
|
|
|
|
:::evidence key="a06-f016-flamingock" alt="잠금 어댑터의 펜스 메서드와 그 javadoc, 실행자 진입점이 지나는 세 관문, 그 파일에서 리스의 펜스를 읽는 곳 전수와 각각의 줄 번호, 원장이 미지정 펜스를 거절하는 자리, 마이그레이션 인터페이스의 멤버 목록, 이 어댑터가 나오는 곳 전수, 실행자와 원장과 잠금의 프로덕션 참조, 그리고 마이그레이션을 적용하는 진입점 전수를 출력한 터미널 기록." caption="펜스는 UNFENCED 를 반환하고 javadoc 은 재개 가능한 것을 거부한다고 적음 · 관문은 보유 검사 74행, 목록 검증 80행, 펜스 검사 82행이고 적용 스트림은 93행 · 펜스를 읽는 곳은 82·154·157행이고 뒤 둘은 원장으로 넘어가 미지정이면 거절 · 인터페이스에 재개 가능성 멤버 없음 · 프로덕션 참조 0 — 73줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
```java
|
|
/**
|
|
* 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.
|
|
*/
|
|
```
|
|
|
|
앞 두 문장은 왜 흉내내지 않았는지를 적는다. 문제는 마지막 문장이 거절 범위를 재개 가능한 마이그레이션으로 한정한 것이다.
|
|
|
|
## 그 문장이 가리키는 장치는 실재한다
|
|
|
|
실행자가 리스의 펜스를 읽는 곳은 셋이다.
|
|
|
|
```text
|
|
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행은 재개 지점을 남긴 마이그레이션에만 걸린다. 그 값을 받은 원장은 미지정이면 거절한다.
|
|
|
|
```java
|
|
private static void requireCurrentFence(long fence, String what) {
|
|
if (fence == MongoMigrationLock.UNFENCED) {
|
|
```
|
|
|
|
재개 가능성에만 반응하는 펜스 조건이 그 자리에 있다. javadoc 의 마지막 문장은 근거 없는 착오가 아니다.
|
|
|
|
## 세 번째 관문이 먼저 던진다
|
|
|
|
```text
|
|
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행이 그보다 앞이다.
|
|
|
|
그 자리에서 나오는 문장도 재개 가능성을 말하지 않는다.
|
|
|
|
```java
|
|
"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"
|
|
:::
|
|
|
|
이 어댑터로 리스를 만들고 아무것도 없는 목록을 적용했다.
|
|
|
|
```text
|
|
[검사가 마이그레이션을 보기 전에 걸린다]
|
|
마이그레이션 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; ...
|
|
```
|
|
|
|
목록이 비어 있으면 재개 가능한지 여부가 존재하지 않는데 같은 문장이 나온다. 한 건을 넣어도 결과는 같다. 그 한 건은 한 번의 호출로 완료를 돌려주므로 체크포인트를 남기지 않는다.
|
|
|
|
같은 마이그레이션을 펜스가 있는 리스로 적용하면 이렇게 된다.
|
|
|
|
```text
|
|
[같은 마이그레이션, 펜스가 있는 리스]
|
|
재개 불가 1건 -> 실행, 결과 1건 COMPLETED
|
|
```
|
|
|
|
거절의 대상은 마이그레이션이 아니라 리스다.
|
|
|
|
## 검사를 옮기는 수정은 두 곳에 막힌다
|
|
|
|
마이그레이션 인터페이스에는 재개 가능성을 선언하는 멤버가 없다.
|
|
|
|
```text
|
|
17: MongoMigrationId id();
|
|
20: MongoMigrationChecksum checksum();
|
|
23: MongoMigrationPrecondition precondition();
|
|
26: MongoMigrationResult execute(MongoMigrationContext context);
|
|
29: MongoMigrationPostcondition postcondition();
|
|
```
|
|
|
|
재개 가능한지는 `execute` 가 체크포인트를 돌려준 뒤에야 드러난다. 그리고 82행을 스트림 안으로 옮겨도 157행이 같은 미지정 값을 원장 기록으로 넘긴다. 원장은 그것을 거절하므로, 데이터베이스는 바뀌고 원장에는 아무것도 남지 않는 상태가 된다.
|
|
|
|
## 이 어댑터가 나오는 곳
|
|
|
|
어댑터를 만드는 곳은 시험 두 곳이다. 하나는 획득 실패를, 다른 하나는 해제와 해제 뒤 갱신 거절을 확인한다. 적용을 부르는 시험은 없어서 javadoc 과 82행의 어긋남이 시험에도 걸리지 않는다.
|
|
|
|
조립 쪽도 같다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 자동 구성이 이 패키지에서 만드는 빈도 없다. 지금 도는 배포를 멈추는 결함이 아니라, 이 리프를 가져다 조립하는 쪽이 처음 실행하는 순간에 드러날 결함이다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
이 어댑터를 실제 배포에서 골랐을 때의 운영 경험은 다루지 않았다. 확인한 것은 세 번째 관문의 조건과 그 조건이 만드는 거절의 범위다.
|
|
|
|
<!-- body:end -->
|