feat(db): wire database tier into the composition root (S04 Step 3)
What
Step 3 of the database composition-root plan: wire the database tier into the running binary. The composition root now builds the app database client and the migration runner, runs migrations in the background, gates /-/readiness on them, removes the dead Step-1 runtime-DSN path, and aligns the S04 spec with the shipped behaviour.
Depends on
- !360 (merged) (plan amendment) must merge first. It reconciles the Step 3 plan text with the design here (background migration, the
defaultBootDepsseam, the client-returning tier orchestration, the file split). Merging this MR before !360 (merged) would put code onmainthat contradicts the plan onmain.
Behaviour
- Database tier wiring.
assembleAppresolves dependencies through a constructor-injection seam (defaultBootDeps/bootOverride/withFatal) and delegates tier wiring towireServices, which callswireDatabase(database clients + readiness checks) then mounts the OCI tier on the returned*postgres.Client. - Background migrations, readiness-gated. The runner is wrapped in an
asyncMigrationRunnerwhoseStartreturns immediately./-/livenessstays up;/-/readinessreports themigrationscheck at 503 until the pass completes, then 200. A failed pass cancels the app context (context.WithCancelCause) and exits non-zero, so Kubernetes restarts and retries. Concurrent replicas serialise on goose's PostgreSQL session advisory lock; each gates its own readiness until it has confirmed the schema under the lock. - Missing database is fatal. No PostgreSQL stanza in the LabKit infrastructure config surfaces
infrastructure.ErrNotConfigured, wrapped with thecomposition:prefix, and aborts boot (exit 1). - Dead-path removal. Deletes the Step-1 runtime
DatabaseConfig/AssembleDSNpath (internal/config/database.go, theConfig.Databasefield, the protodatabasefield — reserved3/database,TLSConfigretained) now that connectivity comes from the infrastructure config.mountOCIis deleted; the client is built in the composition root and passed towireOCI(resolves !155 (merged) note 3406432697).
Tests
Unit (go test -race -short) and integration (-tags=integration, testcontainers PostgreSQL) both pass locally; the full module -tags=integration -race suite is green, including the migration package's advisory-lock contention tests.
Step-3 acceptance coverage
| Plan acceptance | Tests |
|---|---|
Readiness reports migrations + database keys at "ok" |
TestBuildApp_DatabaseReadinessKeysPresent (stub), TestLifecycle_DatabaseIntegration (live) |
End-to-end: boots, migrations applied, /-/readiness 200, clean SIGTERM |
TestLifecycle_DatabaseIntegration |
Existing lifecycle suite green via assembleAppForTest(stubbedDatabase()) |
runApp, TestBuildApp_HappyPath, TestBuildApp_MissingConfigFile, TestBuildApp_InvalidYAML, all TestLifecycle_* |
Missing database fatal: ErrNotConfigured wrapped with composition prefix |
TestDefaultBootDeps_MissingDatabaseIsFatal, TestDefaultBootDeps_MissingDatabaseRunnerIsFatal |
App client built in composition root, passed to wireOCI (note 3406432697) |
TestWireOCI_ProductionEntryPoint (signature pin); mountOCI deleted |
| Background runner: non-blocking Start, ready transition, fatal-on-failure, shutdown-not-fatal | TestAsyncMigrationRunner_NonBlockingStartAndReadyTransition, ..._FailureInvokesOnFatal, ..._ShutdownCancelsWithoutFatal, ..._Name |
| Migration failure → non-zero exit classification | TestRunOutcome |
S04 spec acceptance (owned elsewhere, listed for completeness)
AC-2 (migrations apply / lock serialisation), AC-5 (HasPending) exercised end-to-end here via TestLifecycle_DatabaseIntegration; AC-3/4/6 (Down/Status/out-of-order) and AC-16/17 (layout, simple protocol) are owned by earlier S04 MRs and Steps 1–2. AC-13/14/15 (build-tag split, testcontainers provisioning, per-package isolation) satisfied by main_integration_test.go + the .gitlab-ci.yml test:integration go-list extension.
Out of scope / follow-up
- Dependency-aware readiness for other dependencies (#58) — only this hookup's two checks (
migrations,database) are gated here. - The migration client's
postgres.WithConnectionPool(false)(session-pinned, PgBouncer-safe advisory lock) remains a pending LabKit TODO indatastore.NewMigrationClient.