Eager store resolution — every infrastructure store resolves at startup
Status: binding. Two owner rulings, the second widening the first:
- Owner ruling 2026-08-16 §4 —
docs/product/decisions/20260816-owner-inputs-post-topology-close.md: “I agree with (b), with (a) for stores on the critical path.” - Owner ruling 2026-08-19 (work-queue B12 —
docs/product/decisions/20260818-owner-work-queue.md— resolving the SPRINT_20260722_009 DOC-6 design fork): “extend it everywhere as we already [do]. let’s make it consistent everywhere.” Decoded against item 4’s option letters: the eager-startup-resolution shape (a) extends from critical-path stores to ALL store families in every host; data-plane QA probes © remain the behavioural visibility layer; and the separate doctor-check mechanism (b) — funded as 009 DOC-6 and blocked on the ten-family design fork — is dropped as superseded. For a store that resolves eagerly, a doctor check adds visibility but no protection; consistency is worth more than the residual reporting nuance.
The critical-path criterion below is retained as history and as the severity guide (it still tells you how bad a broken store is — whole data plane vs one feature area) — but it no longer decides whether a store resolves eagerly. Since 2026-08-19 the answer is: it does.
The failure this exists to prevent
On 2026-08-15 packsregistry-web returned HTTP 500 on its entire data plane while reporting Up (healthy). SeedFsPacksRegistryBlobStore’s constructor called Directory.CreateDirectory on a path it cannot write (/app is mode 500 in the hardened base image). Three things combined:
- the constructor had an eager side effect that could fail;
- the DI registration was a lazy singleton factory, so nothing constructed it at startup;
/healthzresolved nothing, so the health probe could not see it.
It was found by accident, while probing an unrelated routing question. Nothing in monitoring would have surfaced it. Two more instances of the same shape were found the same day by sweeping for it.
The posture (since 2026-08-19)
- Every infrastructure store resolves eagerly at startup, in every host that registers it. A store that cannot construct is a loud startup failure, never a healthy container with a dead data plane.
- Data-plane QA probes remain the behavioural layer — eager resolution proves the store constructs, not that its backing medium accepts writes; the QA feature-check probes are what exercise the store end to end.
- The 2026-08-16 default — “(b) a doctor/health check resolves the store and reports without refusing to boot” — is superseded. It was funded as SPRINT_20260722_009 DOC-6 and blocked on a real design gap (storage has ten service-owned interfaces and no universal type to bind, unlike
NpgsqlDataSource); the 2026-08-19 ruling resolves that fork by dropping the mechanism rather than building it.
The criterion — is a store on the critical path? (severity guide)
A store is critical-path when the service’s data plane is unusable without it: if the store cannot construct, substantially every non-health endpoint fails.
Apply it by asking what else stops working, not by how important the store feels:
| Question | Critical-path | Not critical-path |
|---|---|---|
| Which endpoints fail if it cannot construct? | substantially all of them | one, or one feature area |
| Is there a useful degraded mode? | no — the service serves nothing | yes — the rest of the API still works |
| Is it injected by a core repository/service that nearly every handler uses? | yes | no |
Worked examples, all measured rather than judged:
| Store | Verdict | Why |
|---|---|---|
retired IPacksRegistryBlobStore (packsregistry) | CRITICAL — historical defect | The former combined seed/read-write seam was injected by PostgresPackRepository and PostgresAttestationRepository, so every data endpoint failed. Proven: /api/v1/packs, /mirrors, /compliance/summary and the offline-seed export all returned 500. JOB-11 replaced it with the closed immutable-seed reader and upload-store seams inventoried below. |
FileReEnvelopeCheckpointStore (packsregistry) | not critical — but resolves eagerly since 2026-08-19 | Backs exactly one endpoint, POST /api/v1/packs/re-envelope. Under the 2026-08-16 ruling it stayed lazy pending a check; the 2026-08-19 consistency ruling lifted the restriction. Its current constructor is bound to the explicit upload root and deliberately creates/repairs and write-probes its durable control directory, so eager resolution makes an unavailable checkpoint authority fail startup rather than a running re-envelope job. |
IFeedSnapshotBlobStore (replay) | not critical — but resolves eagerly since 2026-08-19 | Backs the four feed-snapshot routes. Same widening as above; same side-effect-free constructor since 3a645c5a8d. |
The criterion is discriminating — it does not classify everything as critical, which is what makes it usable as a severity guide. Since 2026-08-19 it no longer gates eager resolution.
The inventory — every store family, every resolving host
Verified against src/ 2026-08-24 and pinned by EagerStoreResolutionConformanceTests in the architecture pack (src/__Tests/architecture/StellaOps.Architecture.Contracts.Tests/), whose estate sweep fails the build when a NEW store-shaped interface appears without either an eager resolve in every registering host or a recorded not-applicable ruling.
| Store family | Host(s) resolving eagerly | Notes |
|---|---|---|
IImmutablePacksRegistrySeedReader | packsregistry-web | Current closed read-only seam for operator-supplied immutable seed bytes; always registered and explicitly resolved after host build. |
IPacksRegistryUploadStore | packsregistry-web | Current closed durable-mutation seam; always registered as either the atomic upload store or the typed unavailable adapter, and explicitly resolved after host build. |
IReEnvelopeCheckpointStore | packsregistry-web | Joined 2026-08-19 (consistency ruling). |
IFeedSnapshotBlobStore | replay-web | Joined 2026-08-19. The 2026-08-15 defect host (3a645c5a8d). |
IArtifactObjectStore | scanner-web, scanner-worker | RustFs or S3 driver; both constructors are side-effect-free (no network, no filesystem). |
ISbomExportBlobStore | scanner-web | Three registration branches (ObjectStorage / Filesystem / dev fallback); exactly one is always registered. NOTE: the ObjectStorage branch resolves IContentAddressedBlobStore, which scanner-web itself never registers — a Provider=ObjectStorage deployment was broken at first use before and is a loud startup failure now. Live deployments use Filesystem. |
IContentAddressedBlobStore | registry-web, sbomservice, vulnerabilities-web | Independent owner registrations. The hub requires its configured durable symbol CAS and resolves it eagerly; BIN-10 freezes the predecessor BinaryIndex/Symbols registrations. |
IDevPortalOfflineObjectStore | exportcenter-worker | Filesystem-backed; clean constructor. |
IRiskBundleObjectStore | exportcenter-worker | Filesystem-backed; clean constructor. |
IAuditBundlePayloadStore | exportcenter-web | Registered only on the durable (Postgres) audit-bundle path; the in-memory runtime legitimately has none. |
IEvidenceObjectStore | evidence-locker-web, evidence-locker-worker | No constructor-time filesystem write. The FileSystem-kind store creates its temporary and destination directories only on the first real StoreAsync operation, so eager resolution is valid on a read-only deployment. Focused direct-construction and DI-resolution tests pin that contract; a first-write round trip pins directory creation and storage behavior. |
IOciContentStore (release-orchestrator L2) | ro-web | Either the RustFs-backed adapter or the deliberate NoOp fail-safe; both construct. |
IManifestBlobAdmissionStore | ro-web | In-memory implementation — no infrastructure failure mode; resolved for consistency. |
IObjectStoreClient | — not applicable | Never a DI seam: no host registers it. Its production implementation is constructed inline inside ro-web’s L2 factory, whose registered surface (IOciContentStore) is inventoried above. |
How to implement
Resolve after builder.Build() and before any request can be served:
var app = builder.Build();
_ = app.Services.GetService<IMyCriticalStore>();
Three rules, each learned from a real defect:
- Never wrap it in
try/catch. Failing to start is the intended behaviour; a swallowed exception restores exactly the blindness this removes. GetService, notGetRequiredService, when “unregistered” is a legitimate configuration. Some hosts register a store only when its backing feature is configured.GetServicestill constructs the singleton when it is registered — which is the point — without turning a valid feature-off configuration into a boot failure. Packsregistry’s current seed-reader and upload-store seams are both always registered; the upload-disabled posture uses its typed unavailable adapter.- Put it in
Program.cs, not in a hosted service. A hosted service starts after the host is already considered up.
One more rule the widening added:
- Eager resolution does not license unrelated constructor side effects. A read-only/content store must not mkdir, connect, or write merely to prove it can construct. A construction-time filesystem or network side effect is the original 2026-08-15 defect class when the deployment legitimately supplies a read-only backing root. The last known read-only-store instance,
IEvidenceObjectStore’s FileSystem constructor, was closed on 2026-08-19: directory creation remains inStoreAsync, where a write actually requires it (see the inventory). A deliberately writable control authority may instead fail startup on an explicit-root readiness probe when that behavior is part of its recorded contract, asIReEnvelopeCheckpointStorenow does.
What happened to the doctor-check half ((b))
Superseded — owner ruling 2026-08-19 (work-queue B12). The mechanism was funded as SPRINT_20260722_009 DOC-6 and blocked on a real gap: the ruling’s “bind stores the way the doctor context binds the data source” analogy fails because storage has ten service-owned interfaces and no universal type — there is nothing to GetService<T>() for generically. Rather than build a shared marker interface or a config-driven probe SDK across ten families, the owner extended eager resolution everywhere and dropped the check mechanism. The limit that decision accepts, stated honestly: eager resolution proves construction, not writability — a root that mounts read-only still fails at first write, per request. That residual is covered by the AppRootWritabilityConformanceTests static gate (unmounted/:ro roots fail the build) and by data-plane QA probes, not by a runtime health check.
Historical references for the dropped mechanism (kept because the traps generalise to any health check): register the doctor context BEFORE AddServiceDoctorChecks — an implicitly-bound context reads as null and reports HEALTHY; and red-proof any check against the pre-fix code — a check that cannot fail is the defect it was meant to catch. EvidenceLockerWriteCheck (src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Storage/Checks/EvidenceLockerWriteCheck.cs) remains the model of a config-driven write probe, currently unreachable (its plugin family is orphaned — see 009’s Decisions & Risks).
