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.6 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 | high-water-mark-swallowed-a-redelivered-change | high-water mark가 "본 위치"를 뜻해서 재전달된 변경이 영구히 사라졌다 | commit-ambiguity-as-a-result | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:high-water-mark-swallowed-a-redelivered-change | 2026-09-01 | case-high-water-mark-swallowed-a-redelivered-change.body.md |
|
|
|
high-water mark가 "본 위치"를 뜻해서 재전달된 변경이 영구히 사라졌다
change stream 파이프라인의 high-water mark가 이벤트 수신 즉시 전진한다. 그래서 투영이 끝나기 전에 failover가 나면 재전달분이 mark에 걸려 버려지고, 다음 이벤트의 checkpoint가 그것을 지나친다. 구독 상태는 RUNNING이다.
관계
- 모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다 본 적 있지만 완료되지 않은 위치라는 제3의 상태가 없어서 생긴 손실이다.
- 조건이 만족될 수 없는 outbox 사슬 같은 형태의 손실이 다른 리프에서 나타난 사례다.
문제
MongoChangeStreamPipeline 은 이벤트마다 위치를 비교해 앞으로 가는 것만 처리한다.
processOne : advancesPosition 이 false 면 Flux.empty advancesPosition : highWaterMark 를 getAndAccumulate 로 갱신하고 전진 여부를 반환
문제는 이 갱신이 투영 실행 전에 일어난다는 점이다. mark 는 완료된 위치가 아니라 본 위치를 뜻한다. 그리고 resume 이 같은 pipeline 인스턴스를 재사용하므로 그 mark 가 남는다.
결론
worker 하나, 평범한 failover 만으로 변경이 영구히 사라진다. probe 세 개가 그것을 관측했다.
probe C 가 결정적이다. 스트림 1 이 이벤트 E 를 전달하고 투영이 200 밀리초 걸리는 동안 50 밀리초 시점에 재개 가능한 드라이버 오류로 스트림이 끊긴다. 서버가 재개하면 checkpoint 가 E 를 지나지 않았으므로 E 를 다시 보내고 이어서 F 를 보낸다.
관측 결과는 이렇다.
terminal : COMPLETED, opens 2 projector : started 2, completed 1 results : APPLIED 하나 checkpoints saved : token-6 highWaterMark : 6.1 state : RUNNING, runbook 비어 있음
E 의 투영은 시작됐다가 failover 로 취소됐다. 재개 시 파이프라인은 재전달된 E 를 버렸다. E 의 첫 전달 때 이미 mark 가 5.1 로 전진했기 때문이다. 그 뒤 F 가 투영되고 checkpoint 가 저장되면서 저장 위치가 E 를 지나쳤다. change stream 은 checkpoint 가 지난 것을 다시 재생하지 않으므로 E 는 도달 불가능해졌다.
그런데 구독은 RUNNING 이고 runbook 은 비어 있고 호출자의 Flux 는 정상 완료한다. 손실을 알리는 신호가 없다.
probe A 는 같은 손실이 BUSY 청구와 재개 가능 실패의 조합으로도 나타남을 보인다. token-5 는 투영되지 않았는데 checkpoint 는 그것을 지나쳤다.
근본 원인은 상태 어휘다. 본 적 있지만 완료되지 않은 위치라는 제3의 상태가 없어서, 파이프라인은 본 것과 완료한 것을 같은 값으로 다룬다.
검증 환경
OpenJDK : 21.0.12 probe 구성 : in-memory, reactor 와 플랫폼 자체 타입만 사용, 서버 없음 조립 방식 : MongoPlatformAutoConfiguration 과 동일하게 consumer 를 조립 probe 클래스 : 임시로 추가하고 실행 후 제거 production 소스 수정 : x
재현 조건
원문은 final/evidence/raw/135a-mongo-changestream-execution-probes.txt 에 있고, 매니페스트는 135-mongo-changestream-manifest-and-probes.txt 에 있다.
probe C 의 조건은 다음과 같다.
- worker 하나, 중복 제거는 항상 청구를 허용한다.
- 스트림 1 이 클러스터 시각 5.1 의 이벤트 E 를 전달한다.
- 투영은 200 밀리초 걸린다.
- 50 밀리초 시점에 재개 가능한 드라이버 오류로 스트림이 끊긴다. errorLabels 는 ResumableChangeStreamError, code 는 133 이다.
- 스트림 2 가 E 를 재전달하고 이어서 클러스터 시각 6.1 의 F 를 전달한다.
본문
mark가 이벤트 수신 즉시 전진하므로 "완료된 위치"가 아니라 "본 위치"를 뜻하고, resume이 같은 pipeline 인스턴스를 재사용해 mark가 남는다.
mark 가 전진하는 시점
:::evidence key="high-water-mark-swallowed-a-redelivered-change" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true" :::
probe 세 개가 관측한 것
worker 하나·평범한 failover에서 checkpoint 없이 지나간 이벤트를 다음 이벤트의 checkpoint가 추월하고, 그 사이 재전달분을 pipeline이 삼킨다.
아무 신호도 나지 않는다
구독은 RUNNING, runbook 비어 있음, caller의 Flux는 정상 완료.
세 테스트가 각각 절반씩만 본다
"본 적 있지만 완료되지 않은 위치"라는 제3의 상태가 없다.
확인하지 못한 것
probe 는 in-memory 구성이다. 실제 replica set failover 에서 같은 순서가 재현되는지는 확인하지 않았다.