--- kind: CASE slug: high-water-mark-swallowed-a-redelivered-change title: high-water mark가 "본 위치"를 뜻해서 재전달된 변경이 영구히 사라졌다 topic: commit-ambiguity-as-a-result project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:high-water-mark-swallowed-a-redelivered-change evidenceCapturedOn: 2026-09-01 body: case-high-water-mark-swallowed-a-redelivered-change.body.md assets: - key: high-water-mark-swallowed-a-redelivered-change file: ../../../final/evidence/rendered/high-water-mark-swallowed-a-redelivered-change.svg evidence: - ../../../final/evidence/raw/high-water-mark-swallowed-a-redelivered-change.txt source: - 원본 분석 절은 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 에서 같은 순서가 재현되는지는 확인하지 않았다.