Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/commit-ambiguity-as-a-result/case/case-high-water-mark-swallowed-a-redelivered-change.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
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>
2026-09-04 22:51:59 +09:00

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
key file
high-water-mark-swallowed-a-redelivered-change ../../../final/evidence/rendered/high-water-mark-swallowed-a-redelivered-change.svg
../../../final/evidence/raw/high-water-mark-swallowed-a-redelivered-change.txt
원본 분석 절은 final/document.md#4-2 · analysis/06 §67 이다.

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 의 조건은 다음과 같다.

  1. worker 하나, 중복 제거는 항상 청구를 허용한다.
  2. 스트림 1 이 클러스터 시각 5.1 의 이벤트 E 를 전달한다.
  3. 투영은 200 밀리초 걸린다.
  4. 50 밀리초 시점에 재개 가능한 드라이버 오류로 스트림이 끊긴다. errorLabels 는 ResumableChangeStreamError, code 는 133 이다.
  5. 스트림 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 에서 같은 순서가 재현되는지는 확인하지 않았다.