# 복구 패키지가 선언한 것 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 *
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 *
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 *
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 *
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 *
{@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 }