docs(localdb): Phase 2 gate + final Phase 1 task record
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
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# 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 `ILocalDb` per site process at `LocalDb:Path`, **required** (`ValidateOnStart`).
|
||||
A site config missing it fails to boot.
|
||||
- `SiteLocalDbSetup.OnReady` is 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` (not `Program.cs`) so the
|
||||
composition-root tests cover it; only `MapZbLocalDbSync` lives in `Program.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.Meters` is 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.cs`
|
||||
- `tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/ReplicationServiceTests.cs`
|
||||
|
||||
Implementation under deletion:
|
||||
|
||||
- `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/SiteReplicationActor.cs`
|
||||
- `src/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_messages` is 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.db` and
|
||||
`store-and-forward.db` are real, populated, on the volume, and actively replicated
|
||||
while the migration runs.
|
||||
|
||||
## 4. Required inputs before writing the plan
|
||||
|
||||
1. 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
|
||||
`amendments` array in `2026-07-19-localdb-adoption-phase1.md.tasks.json`).
|
||||
2. The two test files in §2, read as specifications.
|
||||
3. **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_messages` is 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.db` interact 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).
|
||||
Reference in New Issue
Block a user