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>
6.4 KiB
Consistency and Transaction Guide
Design §12–§16, decisions D-07 through D-10. This is the part of the platform where the wrong default is most expensive and the least visible in testing, because every failure mode here needs a primary change to reproduce.
1. Prefer a single-document atomic operation
D-09: a transaction is for a multi-document invariant, nothing else. A single document is already atomic in MongoDB, so wrapping a one-document update in a transaction buys nothing and costs a session, a two-phase commit and a new ambiguous outcome.
D-07: partial change uses update operators, not save(). MongoAtomicOperations /
MongoAtomicOperationsTemplate expose the operator set through MongoUpdateOperator
($set, $inc, $push, $pull, $addToSet, $min, $max, $currentDate, …) with an
AtomicFilter precondition and a ReturnDocumentMode. Read-modify-write through save() replaces
the whole document and silently discards any field another writer changed in between — a lost update
with no error.
2. Whole-document replacement needs a revision
D-08. VersionedMongoUpdater requires a MongoRevision: either a Spring Data @Version field or an
explicit expected-revision predicate in VersionedUpdateCommand. A replacement whose filter matched
zero documents is not "nothing to do" — MongoOptimisticConflictTranslator distinguishes:
- filter matched nothing and the id does not exist →
MongoDocumentNotFoundException - filter matched nothing and the id exists →
MongoOptimisticConflictException
Collapsing these two into one is how a concurrent overwrite becomes a 404.
3. Consistency profiles
MongoConsistencyProfile names the read/write concern pair; MongoConsistencyRegistry binds a
profile to an operation or collection, and MongoConsistencyBinder /
ReactiveMongoConsistencyBinder apply it at execution.
| Profile | Meaning | Use for |
|---|---|---|
PRIMARY_LOCAL |
primary read, local concern | Throughput-sensitive reads that tolerate a rollback window. |
PRIMARY_MAJORITY |
primary read, majority write | The default for anything a user will see again immediately. |
CAUSAL_MAJORITY |
majority inside a causal session | Read-your-writes across separate operations. |
STALE_READ_ALLOWED |
secondary reads permitted | Reporting and analytics that state their staleness. |
SNAPSHOT_TRANSACTION |
snapshot isolation | Multi-document reads inside a transaction. |
A profile is a declaration, not a hint: the registry is consulted per operation and an operation without a registered profile is rejected rather than defaulting.
4. Causal sessions
MongoCausalSessionContext plus SpringMongoCausalSessionExecutor /
ReactiveMongoCausalSessionExecutor carry the cluster time and operation time between operations, so
"write then read" returns the write even when the read lands on a different node. Without a causal
session, PRIMARY_MAJORITY gives you durability but not read-your-writes across two calls.
In the reactive path the session travels in the Reactor context (ReactiveMongoContextKeys), not in
a thread local — a thread local is empty on the next operator in the chain.
5. Transactions
MongoTransactionExecutor / ReactiveMongoTransactionExecutor open a session through the session
factory, run the body, and commit. MongoTransactionProfile carries the consistency profile, the
maxCommitTime and the retry budget. Topology matters: a transaction requires a replica set, and
MongoStartupValidator refuses a transaction-declaring profile on STANDALONE at startup rather
than at the first call.
6. Retry: body and commit are different loops
D-10, and the single most consequential rule in the design.
for each body attempt:
open a NEW session
run the body
TransientTransactionError -> abort, next body attempt
commit
UnknownTransactionCommitResult -> retry COMMIT ONLY, same session
MongoTransactionRetryCoordinator implements exactly this:
- A new session per body attempt. Reusing the session after an abort carries the aborted transaction's state into the retry.
- The body is never replayed after a commit ambiguity. An unknown commit means the commit may already have applied. Re-running the body would apply it a second time. Only the commit is retried, and a commit retry on an already-committed transaction is a no-op by design.
- A budget bounds both loops.
MongoRetryBudgetlimits attempts and elapsed time, with jittered backoff (delayBefore(attempt, random)), so a struggling primary is not retried into the ground.
MongoRetryDecision and MongoRetryScope (in …api.error) say what may be retried:
MongoRetryScope.BODY, COMMIT_ONLY, or NONE.
7. Ambiguous outcomes
MongoExecutionOutcome has six values, two of which are ambiguous and must not be collapsed:
| Outcome | Did the write happen? |
|---|---|
NOT_SENT |
No. Safe to retry. |
NO_WRITE_PERFORMED |
No — the server answered and did nothing. |
WRITE_CONFIRMED |
Yes. |
PARTIAL_BULK_WRITE |
Some of it. See MongoBulkResult. |
WRITE_RESULT_UNKNOWN |
Unknown. |
TRANSACTION_COMMIT_UNKNOWN |
Unknown. |
An unknown outcome is not a failure and must not be reported to a caller as one. The caller either
reconciles (MongoCommitReconciler re-reads a deterministic marker the body wrote) or surfaces the
ambiguity. See runbooks/unknown-commit.md.
MongoFailureContext records only the design-permitted fields — outcome, category, operation name,
collection profile, retry scope, attempt — never the query, the document, or the values.
8. Failure translation
DefaultMongoFailureClassifier classifies labels before codes. The server's error labels
(TransientTransactionError, UnknownTransactionCommitResult, RetryableWriteError) are the
authoritative statement about retryability; an error code is a secondary signal whose meaning varies
by server version. DefaultMongoFailureTranslator maps a classification onto the stable exception
hierarchy, and anything unmatched becomes MongoUnclassifiedFailureException rather than leaking a
driver type.
9. Bulk writes
MongoBulkExecutor returns a MongoBulkResult with per-item MongoBulkItemFailure entries. An
unordered bulk write that partially fails is PARTIAL_BULK_WRITE, not a failure: some documents were
written. MongoBulkPartialFailureException carries the succeeded and failed indexes so a caller can
resume rather than replay.