SCRUM-213 — Config: draft read and save with optimistic locking¶
Plan ref: CFG-B3 (docs/11-admin-plane-plan.md). Stacked on SCRUM-212.
What changed¶
| Route | Role | Behaviour |
|---|---|---|
GET /namespaces/{ns}/draft |
viewer | {namespace, document, revision, base_version, updated_by, updated_at}; unknown ns → 404 |
PUT /namespaces/{ns}/draft |
live_ops | body {"document": {…}, "revision": N} → 200 {revision: N+1, updated_at}; stale N → 409 stale_revision "the draft has moved on since revision N"; unknown ns → 404 |
Save is one statement inside a transaction:
UPDATE config_draft SET body=$3, revision=revision+1, … WHERE namespace=$1 AND revision=$2.
Under Postgres's default READ COMMITTED isolation a concurrent loser blocks on the
row lock, re-checks revision=$2 after the winner commits, and matches nothing →
409. The draft.save audit row is written in the same transaction, so a loser
leaves no audit row.
Body rules (400): ≤1 MiB (413 body_too_large); duplicate keys / invalid UTF-8 /
unknown field / trailing data → invalid_body; document must be an object and
revision ≥ 1 → validation_failed. The draft is not schema-validated on save
(it is work in progress); versioning (SCRUM-215) enforces validity.
How to verify¶
cd services/config
export CONFIG_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/config_test?sslmode=disable'
go vet ./... && go test -race -count=1 ./...
go test -race -count=1 -v -run Draft ./internal/...
| Test | Proves |
|---|---|
api draft body tests |
each body rule |
store.TestDraftAndSaveDraft |
fresh draft is rev 1 {}; save at 1 → 2 + one audit row; save at 1 again → ErrStaleRevision, no audit row |
store.TestSaveDraftIsOptimistic |
10 concurrent saves at revision 1 → exactly 1 win, 9 stale, final revision 2, exactly 1 audit row |
server.TestDraftRoutesThroughTheLiveServer |
viewer GET 200, viewer PUT 403, live_ops PUT 200, repeat → 409 stale_revision |
By hand (live_ops token $T):
curl -s -X PUT -H "Authorization: Bearer $T" -d '{"document":{"motd":"hi"},"revision":1}' \
localhost:8080/api/admin/config/namespaces/ui.motd/draft # 200 {"revision":2,…}
# same command again → 409 stale_revision
Results at time of writing¶
go vet,go test -race ./...against Postgres 16: pass (7 packages).
How it was built¶
DeepSeek run scoped (Landlock) to services/config (87 s, ~16k output tokens).
Claude review: store SQL and isolation reasoning checked, concurrency assertions read,
suite re-run with -race against Postgres. No changes needed.