Scratch handoff so the plan can be picked back up cleanly: Task 1 done and its STOP verdict superseded, what must happen before Task 3, the four plan-premise corrections, the rig gotchas that are not discoverable from the repo, and the rig cleanup the Task 20 live gate needs. Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
5.6 KiB
LocalDb Phase 2 — resume state (written 2026-07-20, pre-compaction)
Scratch handoff for picking the plan back up. Delete once Phase 2 lands.
Plan: 2026-07-19-localdb-adoption-phase2.md
(execute with superpowers-extended-cc:executing-plans; task state in
…-phase2.md.tasks.json)
Branch: feat/localdb-phase2 (from feat/localdb-phase1, which is not merged/pushed)
Tree: clean @ 8652eab9. Build 0 warnings.
Where we are
Task 1 (the gate) is DONE. Its STOP verdict has been superseded — Phase 2 is unblocked.
Task 1 appeared to fail on a Phase 1 blocker (LocalDb throwing disk I/O error under load).
That was root-caused the same day as observer-induced and not a product defect: host-side
sqlite3 reads of the live bind-mounted WAL files reset the WAL under the container. The
unmodified library sustains the full soak load indefinitely. Full write-up in
docs/known-issues/2026-07-20-localdb-disk-io-error-under-load.md
§0 (cause) and §11a (fixes shipped).
Tasks 2–21 are untouched. No plan code has been written.
Before starting Task 3
- Task 2 (trivial, ~3 min) — the decision record. Blocked on Task 1's numbers, which are partly missing (below).
- Re-run the cut-short sampling. The soak died before producing sustained
sf_messages/native_alarm_statewrite rates. Use thesnaphelper now in the plan (Task 1 step 2, ~line 217) — copies the.db/-wal/-shmtriplet and queries the copy. Never run host-sidesqlite3against a live file. Both site-a nodes must have been restarted since any host-side read (they have been — see rig state). - Then Wave 1 (Tasks 3 + 4) can be dispatched in parallel.
Findings that change the plan (carry these forward)
From 2026-07-19-localdb-phase2-soak.md:
- D6's premise is wrong, its risk is real. The plan says
config_jsonis "documented to exceed 128 KB per row." The source actually says the escaped Akka envelope blew the 128 KB frame and the raw JSON "only needs to be ~60-70 KB". So the largest known production row is ~60–70 KB → no stop condition. ButMaxBatchSize=500is unsafe (500 × 70 KB ≈ 35 MB vs a 4 MB gRPC cap) → Task 19 must setLocalDb:Replication:MaxBatchSize = 16. This is the one firmly evidence-backed number. - D4 is wrong.
native_alarm_stateis not "unbounded by design" —NativeAlarmActorcoalesces perSourceReferenceon a 100 ms flush (NativeAlarmActor.cs:473-502), so worst case isdistinct_source_refs × 10/sec. sf_messageshas a hard 50 rows/sec ceiling —SweepBatchLimit(500) ÷RetryTimerInterval(10 s). A ceiling, not an estimate.- Task 1 step 7's stop condition is much weaker than written. Exceeding the oplog caps
prunes and flags
needs_snapshot→ graceful snapshot resync (OplogStore.cs:109-138,MaintenanceBackgroundService.cs:57). Not data loss, not a wedge.
The plan's own Task-1 method also needed four corrections (metrics sidecar, safe sampling, no alarm generator on this rig, template 4 undeployable) — all recorded in the soak doc §1.
Rig operational gotchas (hard-won; not obvious from the repo)
ExternalSystem.Calldoes NOT buffer to store-and-forward — onlyExternalSystem.CachedCalldoes. UsingCallgives HTTP traffic and zerosf_messagesrows. This is the single most important detail for generating S&F load.- Never query the bind-mounted SQLite files from the macOS host while containers run. Use
the plan's
snaphelper, ordocker run --rm -v <dir>:/d alpine/sqlite3 …. - No
curlin theaspnet:10.0image, and metrics port 8084 is not published. Use a sidecar:docker run --rm --network container:scadabridge-site-a-a curlimages/curl:latest -s localhost:8084/metrics - Seeded template 4 (
Motor Controller) is not deployable — 34 pre-deployment validation errors. The soak used a purpose-builtSoakGeneratortemplate instead. - CLI:
template script updaterequires--nameand--trigger-typeeven to change only--code. In zsh, don't put auth flags in an unquoted variable (no word-splitting). - Rig auth:
--url http://localhost:9000 --username multi-role --password password.LdapGroupMappingsrole strings must be canonicalDesigner/Deployer; central caches them at startup, so restart central after seeding them.
Rig state as left
Reseeded and healthy; both site-a nodes restarted after the poisoning incident.
Needs cleanup before the Task 20 live gate:
ExternalSystemDefinitionsid 1 is still repointed tohttp://127.0.0.1:9— restore tohttp://scadabridge-restapi:5200.- Template
SoakGenerator(id 2021) and instancessoakgen-1..4(ids 5–8) are still deployed on site-a and still generating load.
Commits this session
| Commit | What |
|---|---|
25463d52 |
plan review pass — corrections from code verification (D1/D3/D6, Task 1 method, Task 14) |
cf46e596 |
fix(rig): seed-sites.sh canonical Designer/Deployer role names |
fc553bd9 |
soak findings (gate verdict, since superseded) |
82c869a1 |
task record: gate verdict |
e9e11d63 |
root-cause brief for the disk-I/O issue |
8652eab9 |
root cause + hardening: extended SQLite error codes, SiteAuditTelemetryActor async-context fix, reseed.sh init-script fix |
Two known flakes, do not chase: InstanceActorChildAttributeRaceTests (intermittent under full-suite
load) and ParentExecutionIdCorrelationTests (~91 s / times out on a cold MSSQL fixture, ~1 s warm).