SCRUM-224 — Patch: load manifests into a lock-free in-memory set¶
Plan ref: PAT-B2 (docs/11-admin-plane-plan.md). Stacked on SCRUM-223.
What changed¶
New internal/manifest:
| Piece | Behaviour |
|---|---|
Loader.LoadAll |
one query (channel_head ⋈ release), every channel; builds a new immutable Set and swaps it in |
Loader.Reload(channel) |
copies the current set, replaces one channel, swaps (used by SCRUM-225's NOTIFY/poll) |
Holder |
atomic.Pointer[Set]; readers do one Load() and a map lookup — no locks on the read path; Ready() is manifests not loaded until the first publish |
Entry |
canonical Body, ETag = "<manifest_sha256>", ReleaseID, MinClientVersion |
| hash check | release.manifest is jsonb, which does not keep the text manifest_sha256 was computed over (Postgres even reorders keys by length). Patch re-derives canonical bytes and refuses a row whose sha256 differs; that channel keeps its last good entry |
Canonical |
identical rule to Config's schema.Canonical (SCRUM-215): keys sorted, no whitespace, floats canonicalised, integers exact |
| wiring | patch.go runs LoadAll at startup (failure logged, startup continues, /readyz stays 503 so an empty DB doesn't crash-loop Patch); /readyz now uses holder.Ready |
| metric | patch_current_release_id{channel} |
How to verify¶
cd services/patch
go vet ./... && go test -race -count=1 ./...
# DB tests read Config's schema (apply services/config migrations to this DB first)
PATCH_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/config_test?sslmode=disable' \
go test -race -count=1 -v ./internal/manifest/
| Test | Proves |
|---|---|
TestCanonical* |
order/whitespace independence, nested sort, 2^53+1 kept, idempotent, duplicate keys / trailing data rejected, the bootstrap migration's body is already canonical, float/int parity with Config |
TestHolderReady, TestSetGet |
readiness before/after; missing channel |
TestPublishedSetIsImmutable |
an old pointer still returns the old entry after a swap |
TestConcurrentLoadAndSwap (-race) |
8 readers vs 1000 swaps: every read is a complete entry whose ETag matches its body's sha256 |
TestLoadAllServesBootstrappedChannels |
dev/staging/live from the real DB; ETag = "+manifest_sha256+" = sha256(body) |
TestLoadAllRejectsTamperedHash |
a row with a wrong sha is not published (done in a rolled-back tx) |
TestLoadAllKeepsLastGoodEntryForARejectedChannel |
a reload that finds a tampered live keeps serving the previous live |
Results at time of writing¶
go vet,go test -race(5 packages) against Postgres 16 with Config's migrations: pass.
How it was built¶
DeepSeek run scoped (Landlock) to services/patch (173 s, ~32k output tokens).
Claude review: LoadAll changed to keep a rejected channel's last good entry (it
dropped it), plus that test and a Config-parity test for Canonical.