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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b5aee50e3
commit
d57d2f62a0
@@ -1,13 +1,195 @@
|
||||
// Driven adapter: opt-in Spring Data MongoDB infrastructure. This leaf owns only enablement and
|
||||
// Mongo client/template auto-configuration; consuming projects add real documents, repositories,
|
||||
// mappings, and ports without shipping a fake business domain in the template.
|
||||
// MongoDB Document Persistence Platform leaf — see
|
||||
// docs/superpowers/specs/2026-08-11-mongodb-document-persistence-platform-design.md (design package)
|
||||
// and docs/mongodb/repository-adaptation.md (how the design's 19 stable + 12 advanced library
|
||||
// modules map here).
|
||||
//
|
||||
// spring-boot-starter-data-mongodb's version is managed by the Spring Boot BOM (applied to every
|
||||
// module in src/build.gradle), so no module-scoped platform is needed.
|
||||
description = 'Outbound adapter: opt-in Spring Data MongoDB infrastructure'
|
||||
// The design models the platform as 19 Stable and 12 Advanced Gradle modules under
|
||||
// `modules/mongodb` and `modules/mongodb-advanced`. This repository's fail-closed 19-leaf registry
|
||||
// (src/config/architecture/modules.json) outranks that layout, so the module boundaries are
|
||||
// packages under dev.caskeleton.adapter.outbound.mongo and MongoModuleBoundaryTest enforces the
|
||||
// design's module dependency table.
|
||||
//
|
||||
// Driver and Spring Data MongoDB versions come from the Spring Boot BOM applied to every module in
|
||||
// src/build.gradle (design §4: "개별 Driver 버전 override 금지"), so nothing here pins them.
|
||||
description = 'Outbound adapter: MongoDB document persistence platform (manifests, atomic writes, ' +
|
||||
'consistency profiles, guardrails, change streams)'
|
||||
|
||||
dependencies {
|
||||
// D1/D2 imperative execution path and the mapping subsystem.
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
|
||||
// D1/D2 reactive execution path: reactive template, cursors and change streams (design §20).
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
|
||||
// The starter package registers auto-configuration and binds typed properties.
|
||||
implementation 'org.springframework.boot:spring-boot-autoconfigure'
|
||||
// Driver-native observability conventions (design §27) publish through Micrometer.
|
||||
implementation 'io.micrometer:micrometer-core'
|
||||
implementation 'org.slf4j:slf4j-api'
|
||||
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
|
||||
// The design's module dependency table is enforced as package rules, so ArchUnit is what keeps
|
||||
// "packages instead of modules" from meaning "no boundary at all".
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
// Real replica set / failover / migration lanes (design §29). Test-scoped so no production
|
||||
// package can reach a container fixture.
|
||||
testImplementation 'org.testcontainers:testcontainers'
|
||||
testImplementation 'org.testcontainers:testcontainers-junit-jupiter'
|
||||
testImplementation 'org.testcontainers:testcontainers-mongodb'
|
||||
testImplementation 'org.testcontainers:testcontainers-toxiproxy'
|
||||
}
|
||||
|
||||
// The testkit is its own source set rather than part of `test` because several lanes consume it and
|
||||
// because the design forbids a production module from depending on the testkit. Declaring its
|
||||
// dependencies only on the test configurations gives that guarantee without a new Gradle project.
|
||||
sourceSets {
|
||||
testkit {
|
||||
java.srcDir 'src/testkit/java'
|
||||
resources.srcDir 'src/testkit/resources'
|
||||
compileClasspath += sourceSets.main.output
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
mongoPerformanceTest {
|
||||
java.srcDir 'src/mongoPerformanceTest/java'
|
||||
compileClasspath += sourceSets.main.output + sourceSets.testkit.output
|
||||
runtimeClasspath += output + compileClasspath
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
// The testkit compiles against exactly what a test does: testImplementation already extends
|
||||
// implementation, so this is the module's own dependencies plus the test libraries.
|
||||
testkitImplementation.extendsFrom testImplementation
|
||||
testkitRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
mongoPerformanceTestImplementation.extendsFrom testImplementation
|
||||
mongoPerformanceTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
}
|
||||
|
||||
// Every test lane compiles and runs against the testkit.
|
||||
sourceSets.test {
|
||||
compileClasspath += sourceSets.testkit.output
|
||||
runtimeClasspath += sourceSets.testkit.output
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testkitImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||
testkitImplementation 'io.projectreactor:reactor-test'
|
||||
testkitImplementation 'org.testcontainers:testcontainers'
|
||||
testkitImplementation 'org.testcontainers:testcontainers-junit-jupiter'
|
||||
testkitImplementation 'org.testcontainers:testcontainers-mongodb'
|
||||
testkitImplementation 'org.testcontainers:testcontainers-toxiproxy'
|
||||
}
|
||||
|
||||
// Pinned server images. The design forbids `latest` for a certification lane (Task 44): a mutable
|
||||
// tag makes a red run unattributable. `-PmongoPrimaryImage=` / `-PmongoCompatibilityImage=`
|
||||
// override them for a one-off run.
|
||||
Closure<Void> applyMongoImageSelection = { task ->
|
||||
task.systemProperty 'mongodb.primary.image',
|
||||
(project.findProperty('mongoPrimaryImage') ?: 'mongo:8.0.16').toString()
|
||||
task.systemProperty 'mongodb.compatibility.image',
|
||||
(project.findProperty('mongoCompatibilityImage') ?: 'mongo:7.0.28').toString()
|
||||
task.systemProperty 'mongodb.toxiproxy.image',
|
||||
(project.findProperty('mongoToxiproxyImage') ?: 'ghcr.io/shopify/toxiproxy:2.12.0').toString()
|
||||
}
|
||||
|
||||
// Docker-backed lanes are excluded from the default unit run: they fail closed without Docker, and
|
||||
// a `check` that fails on a laptop without Docker teaches people to skip `check`.
|
||||
tasks.named('test', Test) {
|
||||
useJUnitPlatform {
|
||||
excludeTags 'quarantine',
|
||||
'mongodb-replicaset',
|
||||
'mongodb-failover',
|
||||
'mongodb-migration',
|
||||
'mongodb-compatibility',
|
||||
'mongodb-security-integration'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('mongoReplicaSetTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Single-node replica set contract lane: mapping, atomic write, transaction, ' +
|
||||
'change stream (design §29).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-replicaset' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.register('mongoFailoverTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Three-node replica set failover lane: primary kill, partition, unknown commit, ' +
|
||||
'resume (design §29).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-failover' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.register('mongoMigrationTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Migration lane: empty / N-1 / oldest-supported snapshots, lock, checkpoint ' +
|
||||
'restart (design §12).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-migration' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.register('mongoCompatibilityTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'MongoDB 7.0 compatibility and 8.0 primary certification matrix (design §30).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-compatibility' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.register('mongoSecurityIntegrationTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'RBAC, TLS, injection and redaction release gate against a real server ' +
|
||||
'(design §26).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-security-integration' }
|
||||
applyMongoImageSelection(it)
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
tasks.register('mongoPerformanceTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Certifies contention, aggregation spill, pagination and pool resource bounds ' +
|
||||
'(design §29).'
|
||||
testClassesDirs = sourceSets.mongoPerformanceTest.output.classesDirs
|
||||
classpath = sourceSets.mongoPerformanceTest.runtimeClasspath
|
||||
useJUnitPlatform()
|
||||
applyMongoImageSelection(it)
|
||||
systemProperty 'performance.assertions.enabled',
|
||||
(project.findProperty('performance.assertions.enabled') ?: 'false').toString()
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
// `check` gains only the hermetic lanes. The Docker-backed ones stay opt-in for the reason above.
|
||||
tasks.named('check') {
|
||||
dependsOn 'mongoStableContractTest'
|
||||
}
|
||||
|
||||
tasks.register('mongoStableContractTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Hermetic stable contract suite: manifests, guardrails, retry scopes, ' +
|
||||
'redaction (design §30).'
|
||||
testClassesDirs = sourceSets.test.output.classesDirs
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
useJUnitPlatform { includeTags 'mongodb-contract' }
|
||||
failOnNoDiscoveredTests = true
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user