Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/analysis-finding-a06-f019.txt
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

209 lines
16 KiB
Plaintext

# 복구 패키지가 선언한 것
MongoChangeHistoryLostException.java
MongoChangeStreamRecoveryDecision.java
MongoChangeStreamRecoveryPolicy.java
MongoInvalidateRecovery.java
MongoChangeStreamRecoveryPolicy.java:7 /**
MongoChangeStreamRecoveryPolicy.java:8 * Decides how a change stream reacts to each kind of failure (design §20.3).
MongoChangeStreamRecoveryPolicy.java:9 *
MongoChangeStreamRecoveryPolicy.java:10 * <p>Three classes with three different answers. A resumable network or election failure resumes
MongoChangeStreamRecoveryPolicy.java:11 * automatically, because the stored token is still valid. An invalidate — a dropped or renamed
MongoChangeStreamRecoveryPolicy.java:12 * collection — needs {@code startAfter} rather than {@code resumeAfter}, which the driver will not
MongoChangeStreamRecoveryPolicy.java:13 * do implicitly. A lost history needs a human.
MongoChangeStreamRecoveryPolicy.java:14 */
MongoChangeStreamRecoveryPolicy.java:15 public final class MongoChangeStreamRecoveryPolicy {
MongoChangeStreamRecoveryPolicy.java:16
MongoChangeStreamRecoveryPolicy.java:17 /** Runbook for a subscription whose oplog history was lost. */
MongoChangeStreamRecoveryPolicy.java:18 public static final String HISTORY_LOST_RUNBOOK = "docs/mongodb/runbooks/history-lost.md";
MongoChangeStreamRecoveryPolicy.java:19
MongoChangeStreamRecoveryPolicy.java:20 /** Runbook for a subscription that failed for a non-resumable reason. */
MongoChangeStreamRecoveryPolicy.java:21 public static final String FAILURE_RUNBOOK = "docs/mongodb/runbooks/failover.md";
MongoChangeStreamRecoveryPolicy.java:22
MongoChangeStreamRecoveryPolicy.java:23 /**
MongoChangeStreamRecoveryPolicy.java:24 * The decision for a lost oplog history.
MongoChangeStreamRecoveryPolicy.java:25 *
MongoChangeStreamRecoveryPolicy.java:26 * <p>Never auto-resumes. The alternative — restarting from the current time — produces a
MongoChangeStreamRecoveryPolicy.java:27 * projection that is missing an unknown range of changes and reports itself as healthy.
MongoChangeStreamRecoveryPolicy.java:28 */
MongoChangeStreamRecoveryPolicy.java:29 public MongoChangeStreamRecoveryDecision onHistoryLost(String subscriptionProfile) {
MongoChangeStreamRecoveryPolicy.java:30 Objects.requireNonNull(subscriptionProfile, "subscriptionProfile");
MongoChangeStreamRecoveryPolicy.java:31 return MongoChangeStreamRecoveryDecision.halt(
MongoChangeStreamRecoveryPolicy.java:32 MongoChangeStreamState.HISTORY_LOST, HISTORY_LOST_RUNBOOK);
MongoChangeStreamRecoveryPolicy.java:33 }
MongoChangeStreamRecoveryPolicy.java:34
MongoChangeStreamRecoveryPolicy.java:35 /** The decision for a resumable failure such as a primary failover. */
MongoChangeStreamRecoveryPolicy.java:36 public MongoChangeStreamRecoveryDecision onResumableFailure() {
MongoChangeStreamRecoveryPolicy.java:37 return MongoChangeStreamRecoveryDecision.resume();
MongoChangeStreamRecoveryPolicy.java:38 }
MongoChangeStreamRecoveryPolicy.java:39
MongoChangeStreamRecoveryPolicy.java:40 /**
MongoChangeStreamRecoveryPolicy.java:41 * The decision for an invalidate event.
MongoChangeStreamRecoveryPolicy.java:42 *
MongoChangeStreamRecoveryPolicy.java:43 * <p>Resumes, but the checkpoint the caller stores must be a {@code startAfter} position — the
MongoChangeStreamRecoveryPolicy.java:44 * subscription is otherwise unresumable, because {@code resumeAfter} rejects an invalidate token.
MongoChangeStreamRecoveryPolicy.java:45 */
MongoChangeStreamRecoveryPolicy.java:46 public MongoChangeStreamRecoveryDecision onInvalidate() {
MongoChangeStreamRecoveryPolicy.java:47 return MongoChangeStreamRecoveryDecision.resume();
MongoChangeStreamRecoveryPolicy.java:48 }
MongoChangeStreamRecoveryPolicy.java:49
MongoChangeStreamRecoveryPolicy.java:50 /** The decision for a failure the platform cannot classify as resumable. */
MongoChangeStreamRecoveryPolicy.java:51 public MongoChangeStreamRecoveryDecision onFailure(MongoDriverFailureView failure) {
MongoChangeStreamRecoveryPolicy.java:52 Objects.requireNonNull(failure, "failure");
MongoChangeStreamRecoveryPolicy.java:53 if (failure.hasServerCode() && isHistoryLost(failure.serverCode())) {
MongoChangeStreamRecoveryPolicy.java:54 return MongoChangeStreamRecoveryDecision.halt(
MongoChangeStreamRecoveryPolicy.java:55 MongoChangeStreamState.HISTORY_LOST, HISTORY_LOST_RUNBOOK);
MongoChangeStreamRecoveryPolicy.java:56 }
MongoChangeStreamRecoveryPolicy.java:57 if (failure.hasLabel("ResumableChangeStreamError")) {
MongoChangeStreamRecoveryPolicy.java:58 return MongoChangeStreamRecoveryDecision.resume();
MongoChangeStreamRecoveryPolicy.java:59 }
MongoChangeStreamRecoveryPolicy.java:60 return MongoChangeStreamRecoveryDecision.halt(MongoChangeStreamState.FAILED, FAILURE_RUNBOOK);
MongoChangeStreamRecoveryPolicy.java:61 }
MongoChangeStreamRecoveryPolicy.java:62
MongoChangeStreamRecoveryPolicy.java:63 /** {@code ChangeStreamHistoryLost} and {@code ChangeStreamFatalError}. */
MongoChangeStreamRecoveryPolicy.java:64 private static boolean isHistoryLost(int serverCode) {
MongoChangeStreamRecoveryPolicy.java:65 return serverCode == 286 || serverCode == 280;
MongoChangeStreamRecoveryPolicy.java:66 }
# 각 진입점의 프로덕션 호출 계수
onFailure main 1 · test 3
onHistoryLost main 0 · test 1
onResumableFailure main 0 · test 1
onInvalidate main 0 · test 0
requireCorrectResumeOption main 1 · test 1
checkpointFor main 0 · test 1
autoResumable main 0 · test 3
new MongoChangeHistoryLost.. main 0 · test 0
[대조] onInvalidate 라는 이름이 나오는 줄 전부 : 2 개
그 예외 이름이 자바 소스에 나오는 줄 : 2 개
main · MongoChangeHistoryLostException.java:16 public final class MongoChangeHistoryLostException extends MongoPersistenceException {
main · MongoChangeHistoryLostException.java:22 public MongoChangeHistoryLostException(
같은 이름을 문서가 쓰는 자리 :
docs/architecture/mongo-api-surface.txt:168 dev.caskeleton.adapter.outbound.mongo.changestream.recovery.MongoChangeHistoryLostException
docs/mongodb/change-stream-guide.md:71 | Token no longer in the oplog | `halt(HISTORY_LOST, "history-lost")` → `MongoChangeHistoryLostException` |
docs/mongodb/change-stream-guide.md:75 `MongoChangeHistoryLostException` is raised when the resume token predates the oldest oplog entry.
docs/mongodb/runbooks/history-lost.md:23 - `MongoChangeHistoryLostException`.
# 쓰이지 않는 예외가 자기 존재 이유로 적은 것
MongoChangeHistoryLostException.java:8 /**
MongoChangeHistoryLostException.java:9 * The oplog no longer contains the stored resume position (design §20.3).
MongoChangeHistoryLostException.java:10 *
MongoChangeHistoryLostException.java:11 * <p>A dedicated type because the recovery is a business decision, not a technical one. The
MongoChangeHistoryLostException.java:12 * platform could resume from "now" — the driver makes it easy — and the projection would silently
MongoChangeHistoryLostException.java:13 * miss every change that fell off the oplog. The only honest options are rebuilding the projection
MongoChangeHistoryLostException.java:14 * or reconciling it against the source, and both need someone to choose.
MongoChangeHistoryLostException.java:15 */
MongoChangeHistoryLostException.java:16 public final class MongoChangeHistoryLostException extends MongoPersistenceException {
MongoChangeHistoryLostException.java:17
MongoChangeHistoryLostException.java:18 @Serial private static final long serialVersionUID = 1L;
MongoChangeHistoryLostException.java:19
MongoChangeHistoryLostException.java:20 private final String subscriptionProfile;
MongoChangeHistoryLostException.java:21
MongoChangeHistoryLostException.java:22 public MongoChangeHistoryLostException(
MongoChangeHistoryLostException.java:23 MongoFailureContext failureContext, String subscriptionProfile) {
MongoChangeHistoryLostException.java:24 super(
MongoChangeHistoryLostException.java:25 "the change stream's resume position is no longer in the oplog; consumption stopped rather "
MongoChangeHistoryLostException.java:26 + "than restarting from now and silently skipping the gap",
MongoChangeHistoryLostException.java:27 failureContext);
MongoChangeHistoryLostException.java:28 this.subscriptionProfile = Objects.requireNonNull(subscriptionProfile, "subscriptionProfile");
MongoChangeHistoryLostException.java:29 }
# 실제로 history lost 를 다루는 자리
MongoChangeStreamRecoveryPolicy.java:50 /** The decision for a failure the platform cannot classify as resumable. */
MongoChangeStreamRecoveryPolicy.java:51 public MongoChangeStreamRecoveryDecision onFailure(MongoDriverFailureView failure) {
MongoChangeStreamRecoveryPolicy.java:52 Objects.requireNonNull(failure, "failure");
MongoChangeStreamRecoveryPolicy.java:53 if (failure.hasServerCode() && isHistoryLost(failure.serverCode())) {
MongoChangeStreamRecoveryPolicy.java:54 return MongoChangeStreamRecoveryDecision.halt(
MongoChangeStreamRecoveryPolicy.java:55 MongoChangeStreamState.HISTORY_LOST, HISTORY_LOST_RUNBOOK);
MongoChangeStreamRecoveryPolicy.java:56 }
MongoChangeStreamRecoveryPolicy.java:57 if (failure.hasLabel("ResumableChangeStreamError")) {
MongoChangeStreamRecoveryPolicy.java:58 return MongoChangeStreamRecoveryDecision.resume();
MongoChangeStreamRecoveryPolicy.java:59 }
MongoChangeStreamRecoveryPolicy.java:60 return MongoChangeStreamRecoveryDecision.halt(MongoChangeStreamState.FAILED, FAILURE_RUNBOOK);
MongoChangeStreamRecoveryPolicy.java:61 }
소비자가 그 결정을 받는 자리 :
ReactiveMongoChangeStreamConsumer.java:119 invalidates.requireCorrectResumeOption(checkpoint, checkpoint.position());
ReactiveMongoChangeStreamConsumer.java:214 return recovery.onFailure(MongoDriverFailureView.from(driverFailure));
history lost 일 때 호출자가 실제로 받는 것 :
ChangeStreamConsumerLifecycleTest.java:108
ChangeStreamConsumerLifecycleTest.java:109 @Test
ChangeStreamConsumerLifecycleTest.java:110 @DisplayName("a lost history stops the subscription and names the runbook")
ChangeStreamConsumerLifecycleTest.java:111 void aLostHistoryStopsTheSubscription() {
ChangeStreamConsumerLifecycleTest.java:112 RecordingSource source = new RecordingSource(Flux.error(queryFailure(286, "")));
ChangeStreamConsumerLifecycleTest.java:113 ReactiveMongoChangeStreamConsumer consumer = consumer(source);
ChangeStreamConsumerLifecycleTest.java:114
ChangeStreamConsumerLifecycleTest.java:115 StepVerifier.create(consumer.run()).verifyError(MongoQueryException.class);
ChangeStreamConsumerLifecycleTest.java:116
ChangeStreamConsumerLifecycleTest.java:117 assertThat(consumer.state()).isEqualTo(MongoChangeStreamState.HISTORY_LOST);
ChangeStreamConsumerLifecycleTest.java:118 assertThat(consumer.requiredRunbook())
ChangeStreamConsumerLifecycleTest.java:119 .isEqualTo(MongoChangeStreamRecoveryPolicy.HISTORY_LOST_RUNBOOK);
ChangeStreamConsumerLifecycleTest.java:120 assertThat(source.opens)
ChangeStreamConsumerLifecycleTest.java:121 .as("restarting from now would hide the gap, not close it")
ChangeStreamConsumerLifecycleTest.java:122 .isEqualTo(1);
ChangeStreamConsumerLifecycleTest.java:123 assertThat(checkpoints.saved).isEmpty();
ChangeStreamConsumerLifecycleTest.java:124 }
# 자기 자신과 견주는 검사
MongoInvalidateRecovery.java:24
MongoInvalidateRecovery.java:25 /**
MongoInvalidateRecovery.java:26 * Rejects a checkpoint that would be replayed with the wrong resume option.
MongoInvalidateRecovery.java:27 *
MongoInvalidateRecovery.java:28 * @throws IllegalStateException when an invalidate checkpoint is about to be used with {@code
MongoInvalidateRecovery.java:29 * resumeAfter}
MongoInvalidateRecovery.java:30 */
MongoInvalidateRecovery.java:31 public void requireCorrectResumeOption(
MongoInvalidateRecovery.java:32 MongoResumeCheckpoint checkpoint, MongoResumePosition intended) {
MongoInvalidateRecovery.java:33 Objects.requireNonNull(checkpoint, "checkpoint");
MongoInvalidateRecovery.java:34 Objects.requireNonNull(intended, "intended");
MongoInvalidateRecovery.java:35 if (checkpoint.position() != intended) {
MongoInvalidateRecovery.java:36 throw new IllegalStateException(
MongoInvalidateRecovery.java:37 "checkpoint for subscription '"
MongoInvalidateRecovery.java:38 + checkpoint.subscriptionProfile()
MongoInvalidateRecovery.java:39 + "' must be replayed with "
MongoInvalidateRecovery.java:40 + checkpoint.position()
MongoInvalidateRecovery.java:41 + " but "
MongoInvalidateRecovery.java:42 + intended
MongoInvalidateRecovery.java:43 + " was requested");
MongoInvalidateRecovery.java:44 }
MongoInvalidateRecovery.java:45 }
그 메서드를 부르는 자리 전부 :
main · ReactiveMongoChangeStreamConsumer.java:119 invalidates.requireCorrectResumeOption(checkpoint, checkpoint.position());
main · MongoInvalidateRecovery.java:31 public void requireCorrectResumeOption(
test · MongoChangeStreamRecoveryPolicyTest.java:59 () -> recovery.requireCorrectResumeOption(checkpoint, MongoResumePosition.RESUME_AFTER))
# 상태 열거형이 그 구분을 어떻게 적는가
MongoChangeStreamState.java:1 package dev.caskeleton.adapter.outbound.mongo.changestream;
MongoChangeStreamState.java:2
MongoChangeStreamState.java:3 /**
MongoChangeStreamState.java:4 * The lifecycle state of one change stream subscription (design §20.1).
MongoChangeStreamState.java:5 *
MongoChangeStreamState.java:6 * <p>{@code HISTORY_LOST} is separate from {@code FAILED} because it is the one state the platform
MongoChangeStreamState.java:7 * refuses to recover from on its own. Resuming from "now" after the oplog has rolled past the
MongoChangeStreamState.java:8 * stored token silently discards every change in between — the projection then looks healthy and is
MongoChangeStreamState.java:9 * quietly wrong, which is worse than a stopped consumer somebody has to look at.
MongoChangeStreamState.java:10 */
MongoChangeStreamState.java:11 public enum MongoChangeStreamState {
MongoChangeStreamState.java:12
MongoChangeStreamState.java:13 /** Opening the stream and resolving the resume position. */
MongoChangeStreamState.java:14 STARTING,
MongoChangeStreamState.java:15
MongoChangeStreamState.java:16 /** Consuming events. */
MongoChangeStreamState.java:17 RUNNING,
MongoChangeStreamState.java:18
MongoChangeStreamState.java:19 /** Recovering from a resumable failure such as a primary failover. */
MongoChangeStreamState.java:20 RESUMING,
MongoChangeStreamState.java:21
MongoChangeStreamState.java:22 /** The oplog no longer contains the stored resume position. Consumption stops. */
MongoChangeStreamState.java:23 HISTORY_LOST,
MongoChangeStreamState.java:24
MongoChangeStreamState.java:25 /** A non-resumable failure. Consumption stops. */
MongoChangeStreamState.java:26 FAILED,
MongoChangeStreamState.java:27
MongoChangeStreamState.java:28 /** Stopped deliberately. */
MongoChangeStreamState.java:29 STOPPED;
MongoChangeStreamState.java:30
MongoChangeStreamState.java:31 /** True when the platform may resume without an operator decision. */
MongoChangeStreamState.java:32 public boolean autoResumable() {
MongoChangeStreamState.java:33 return this == RUNNING || this == RESUMING || this == STARTING;
MongoChangeStreamState.java:34 }
MongoChangeStreamState.java:35 }