Files
clean-architecture-backend-…/docs/mongodb/runbooks/failover.md
T
DongHyeonkaandClaude Opus 5 d57d2f62a0 feat(mongodb): implement the MongoDB document persistence platform
Implements the mongodb-superpowers-package design: Stable Tasks 1-50 and
Advanced Tasks 1-15.

The design assumes 19 Stable + 12 Advanced Gradle projects under
modules/mongodb*. This repository's fail-closed registry declares exactly 19
leaf identities, so those modules become package boundaries inside the
registered leaf :adapter:outbound:persistence-mongo, with the design's module
dependency table enforced by ten ArchUnit rules. The mapping and every
deviation are recorded in docs/mongodb/repository-adaptation.md.

Contract highlights, all enforced by tests rather than convention:

- Transaction body retry and commit retry are separate loops. A new session per
  body attempt; commit-only retry on an unknown commit. The body is never
  replayed after a commit ambiguity, so a failover cannot become a duplicate.
- MongoExecutionOutcome keeps both ambiguous outcomes distinct from success and
  failure, and MongoFailureContext records only the design-permitted fields.
- Failure classification reads server error labels before numeric codes.
- BSON representations come from a pinned manifest, never a library default,
  and a golden type-signature gate fails on any drift.
- Index and validator changes go through the manifest and the admin plane;
  metadata ownership gates every drop.
- Every Advanced capability refuses construction unless its flag is enabled.

Verified against real servers, not only unit tests. Running the lanes for the
first time exposed four defects that a green `check` had hidden:

- Four release lanes passed while executing zero tests; the gate now counts
  executed tests per lane and fails on zero.
- The "single replica set" fixture was a standalone, because Testcontainers 2.x
  needs withReplicaSet(); its test only asserted a connection string.
- The three-node fixture was three independent clusters, so no election could
  occur, and awaitNewPrimary() compared against the post-stop primary.
- The migration lease checked modifiedCount, so a same-millisecond refresh read
  as a lost lease.

scripts/verify-mongodb-platform.sh now reports:
  9 lanes, 0 skipped, 0 failed, every evidence category produced.

scripts/verify-mongodb-advanced.sh reports NOT PROMOTABLE: actual-topology
evidence (real sharded cluster, real KMS, real target deployment) is
unobtainable here, so it is named rather than assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:41:00 +09:00

4.7 KiB

title, category, severity, owner, last_updated, status
title category severity owner last_updated status
Runbook — MongoDB primary failover mongodb P2 oncall 2026-08-13 active

Runbook: MongoDB primary failover

Design §29, scenarios PRIMARY_KILL, NETWORK_PARTITION, SERVER_SELECTION_TIMEOUT, WRITE_RESPONSE_LOSS.

Symptoms

  • MongoServerSelectionException / MongoConnectionException spike, then recovery within seconds.
  • MongoSdamObservationListener reports a topology change (primary removed, new primary elected).
  • MongoPoolObservationListener shows checkout wait times rising while server-side command duration stays flat — the wait is topology, not query cost.
  • Latency spike on writes with no corresponding rise in read latency.

A failover that resolves in under ~15 s and produces no WRITE_RESULT_UNKNOWN is normal replica-set behaviour and needs no action beyond confirming it self-healed.

Diagnosis

  1. Confirm an election actually happened. SDAM events distinguish an election from "the database got slow"; without them the two are indistinguishable in application metrics.
  2. Split the failure categories. Metric tag failureCategory:
    • SERVER_SELECTION / CONNECTION → the driver could not reach a primary. NOT_SENT; safe.
    • TIMEOUT with outcome WRITE_RESULT_UNKNOWN → a write may have applied. Not safe; see below.
    • TRANSACTION_COMMIT_UNKNOWN → go to unknown-commit.md instead.
  3. Check the election duration against MongoRetryBudget. If the election outlasted the budget, the retries were exhausted before a primary existed and callers saw errors that a longer budget would have absorbed.
  4. Check whether the new primary is in the expected region/AZ. A failover to a distant node changes write latency permanently, not transiently.

Action

Self-healed (the common case). Confirm outcome distribution contains no WRITE_RESULT_UNKNOWN, record the election in the incident log, and close. Nothing to replay.

Writes with WRITE_RESULT_UNKNOWN. These writes may or may not have applied. Do not blind-retry.

  • Idempotent operation (registered MongoUpdateOperator with an AtomicFilter precondition): retry. The precondition makes the second application a no-op.
  • Non-idempotent operation: reconcile by reading the target document and comparing against the intended post-state. Retry only if it does not reflect the write.

Server selection never recovers. The set has lost quorum — two of three nodes are down or partitioned. No client-side action fixes this; escalate to the database owner to restore a majority. The application should be failing closed, not queueing.

Elections are frequent (more than one a day, unprompted). This is an infrastructure symptom, not an application one: check node resource saturation, disk latency on the primary, and network stability between members. Repeated elections cause repeated unknown-outcome windows.

Escalation

  • P2 → P1 if server selection has failed for more than 2 minutes, or if any non-idempotent write returned WRITE_RESULT_UNKNOWN and cannot be reconciled.
  • Page the database owner for quorum loss, and the service owner for reconciliation of ambiguous writes.

Verification

The failover lane reproduces this deliberately:

cd src
./gradlew :adapter:outbound:persistence-mongo:mongoFailoverTest --console=plain

It starts a real three-node set (MongoThreeNodeReplicaSet), stops the primary (MongoPrimaryController), and injects network faults through Toxiproxy (ToxiproxyMongoNetworkFaultController). A single-node set is not sufficient for the election: it never holds one, so every guarantee that depends on a primary change goes untested.

The network faults need their own fixture (MongoProxiedReplicaSetNode) because a stopped container cannot produce them. Stopping a node tells the client the write did not happen; cutting the path while the server keeps running produces a client that cannot tell. MongoNetworkFaultLaneTest asserts the difference by reaching the same server twice — once through the proxy, once directly:

  • Partition: the proxied client fails, the direct client finds the server healthy and the earlier write intact. The path was cut, not the server.
  • Response loss: the proxied client fails, and the direct client then finds the document present. The write applied and only the acknowledgement was lost — DefaultMongoFailureClassifier returns WRITE_RESULT_UNKNOWN, and a retry would have inserted a second document.

One detail the lane depends on: the connection is warmed before the toxic is applied. On a cold connection it is the driver's handshake whose response is dropped, so the write is never transmitted — NOT_SENT, the opposite of the ambiguity being tested.