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