Task 14. A GATE document, not an implementation plan - the plan explicitly wants Phase 2 planned against post-Phase-1 reality, which means after this record exists rather than from the original design alone. Records what Phase 1 actually established (ordering constraints, where registration lives and why, the fail-closed interceptor, the silent meter allowlist), what must be ported before anything is deleted (both bespoke replicator test files, treated as specifications, plus the accepted N5 bounded-duplicate race), and why Phase 2 is a harder risk class than Phase 1: it REPLACES a working mechanism rather than adding one where none existed, sf_messages has external side effects, config tables drive deployment, there is no dual-mechanism period, and the migration is real this time rather than the usual no-op. The most important line is an open question, not an answer: choosing MaxOplogRows / MaxOplogAge / snapshot-resync thresholds for sf_messages write rates needs a Phase-1 rig SOAK that has not been run. The live gate proved correctness, not steady-state behaviour over time. Phase 2 should not be planned without it. tasks.json finalized: all 15 tasks completed, every deviation from the plan recorded with its reason. Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
5.7 KiB
LocalDb Phase 2 — Gate Document
Status: NOT STARTED. This is the gate, not the plan. Phase 2 work must not begin until an implementation plan is written against the facts below and the open questions in §5 are answered.
Phase 1 live gate: PASS (2026-07-19, docker rig). See the Task 12 commit for evidence.
Phase 2 goal (from the design doc, scadaproj docs/plans/2026-07-19-scadabridge-localdb-design.md §1):
move scadabridge.db's config tables and StoreAndForward's sf_messages into the same
consolidated LocalDb file, then delete SiteReplicationActor and StoreAndForward's
ReplicationService outright. CDC triggers replace hand-shipped ops; the library's
snapshot resync replaces the bespoke chunked anti-entropy. The design explicitly chose
replace + delete, no dual-mechanism period — recoverable from git.
1. What Phase 1 actually established
Facts a Phase 2 plan can rely on, all verified rather than assumed:
- One
ILocalDbper site process atLocalDb:Path, required (ValidateOnStart). A site config missing it fails to boot. SiteLocalDbSetup.OnReadyis the single place tables are created and registered. Ordering is load-bearing: DDL →RegisterReplicated→ writes, then the legacy migrator. Rows written before registration are invisible to the peer forever, silently.- Replication is registered in
SiteServiceRegistration(notProgram.cs) so the composition-root tests cover it; onlyMapZbLocalDbSynclives inProgram.cs. - Inbound auth is
LocalDbSyncAuthInterceptor, fail-closed, scoped to/localdb_sync.v1.LocalDbSync/, sharing the existing 8083 h2c listener. - Default-OFF is real and proven side-by-side: rig site-a replicates, site-b/site-c do not.
ZbTelemetryOptions.Metersis a silent allowlist (SiteServiceRegistration.ObservedMeters).- Bidirectional sync works with only one node configured as initiator.
2. What Phase 2 must port before deleting anything
The bespoke replicator's tests encode behaviour that the design's CDC replacement must still satisfy. Port the test intents first, delete second — the plan should treat the existing tests as the specification, not as code to be removed alongside the implementation.
tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/SiteReplicationActorTests.cstests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/ReplicationServiceTests.cs
Implementation under deletion:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReplicationActor.cssrc/ZB.MOM.WW.ScadaBridge.StoreAndForward/ReplicationService.cs
Known accepted behaviour to carry forward, not silently drop: the N5 bounded-duplicate race the bespoke replicator documents and accepts. A Phase 2 plan must state whether CDC inherits the same bound, a tighter one, or a different failure shape.
3. Substantially harder than Phase 1
Phase 1 moved two tables that nothing replicated before, so the worst case was "no worse than today". Phase 2 replaces a working mechanism, which makes it a different risk class:
sf_messagesis an outbound buffer with side effects. Phase 1's tables are read-mostly records; a store-and-forward row that resurrects or double-delivers sends real traffic to an external system. Park/requeue/remove becoming ordinary captured row updates/deletes needs its own analysis of what LWW does to an in-flight send.- Config tables drive deployment. A converged-but-wrong config row is a deployed instance behaving incorrectly, not a missing history entry.
- No dual-mechanism period. The chosen posture means the cutover is the test. That raises the bar on the offline convergence suite and the live gate considerably.
- Migration is not a no-op this time. Phase 1's migrator usually finds nothing,
because the legacy files sat outside the volume.
scadabridge.dbandstore-and-forward.dbare real, populated, on the volume, and actively replicated while the migration runs.
4. Required inputs before writing the plan
- The design doc §1 Phase 2 and §2 schema/migration sections, re-read against
post-Phase-1 reality (several Phase 1 assumptions changed during execution — see the
amendmentsarray in2026-07-19-localdb-adoption-phase1.md.tasks.json). - The two test files in §2, read as specifications.
- Rig soak observations that do not exist yet — see §5.
5. Open questions — answer these first
- Oplog growth and churn under real config/S&F write rates. Phase 1's tables are
low-volume;
sf_messagesis not.MaxOplogRows/MaxOplogAge/ snapshot-resync thresholds cannot be chosen from first principles. This needs a Phase-1 rig soak that has not been run. The live gate proved correctness, not steady-state behaviour over time. - LWW semantics for in-flight store-and-forward sends. What does last-writer-wins do when one node parks a message the other is mid-delivery on?
- Migration-under-load. How does the one-time copy of an actively-replicating
scadabridge.dbinteract with the bespoke replicator still running during cutover? - Rollback story. With no dual-mechanism period, what is the recovery path if the cutover fails in production — beyond "revert the commit"?
- Does the consolidated file stay appropriate? One-DB-per-process was chosen partly because Phase 1's tables were small. Adding config + S&F changes the size and write profile of that single file.
6. Not in scope (unchanged from the design)
auditlog.db (diverges per node by design — central pulls the union; replicating it would
double-forward), the secrets store (has its own answer in Secrets.Replicator.SqlServer
hub mode), and central nodes (SQL-Server-first, no LocalDb use case).