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>
209 lines
16 KiB
Plaintext
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 }
|