docs(mesh-phase4): ServiceLevel semantics on DB-less nodes + driver ConfigDb cut

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-23 13:18:57 -04:00
parent 6f21213f70
commit 55a6e1f2b2
5 changed files with 116 additions and 15 deletions
@@ -141,7 +141,7 @@ admin-change refresh), clusters know central from appsettings (static, restart t
flow over ClusterClient (DPS paths deleted or dark-switched), including an Ask timing out cleanly
against a stopped node.
### Phase 3 — Config fetch-and-cache from central — **CODE-COMPLETE 2026-07-23 (Tasks 08; live gate Task 10 pending)**
### Phase 3 — Config fetch-and-cache from central — **DONE 2026-07-23 (merged `d01b0695`, live gate PASSED, pushed to origin)**
**Decisions taken:** the deploy artifact is served **by central over a gRPC fetch RPC**
(`DeploymentArtifactService.Fetch`, streamed chunks — chosen over token-gated HTTP), authenticated by
a **shared node bearer key** (`ConfigServe:ApiKey == ConfigSource:ApiKey`, `FixedTimeEquals`,
@@ -160,13 +160,14 @@ config SQL read); #485 coverage; and a **real two-Host gRPC boundary test** (rig
wrong-key control returns null, failover). Chunking, SHA-256 verify, newest-2 retention, pair
replication carry over unchanged. **Phase boundary held:** config **reads** only — the
`NodeDeploymentState` ack-row write, `DbHealthProbe`, `EfAlarmConditionStateStore` stay for Phase 4.
**Remaining (Tasks 910):** rig config + docs (done) and the live gate — deploy on a site pair flipped
to `FetchAndCache` with central SQL stopped mid-fetch retry lands; #485 last-known-good re-proven on
the new path.
**Exit gate:** live gate green on the rig; a driver node with an empty LocalDb and reachable central
boots into the current config; with central down it boots last-known-good.
**Live gate PASSED:** deployed on a site pair flipped to `FetchAndCache` with central SQL stopped
mid-fetch retry landed; #485 last-known-good re-proven on the new path. Merged to master
`d01b0695` and pushed to origin; the scadaproj umbrella index was updated + pushed (`b5e7bc8`) in the
same pass.
**Exit gate — MET:** live gate green on the rig; a driver node with an empty LocalDb and reachable
central boots into the current config; with central down it boots last-known-good.
### Phase 4 — Cut the driver-side ConfigDb connection
### Phase 4 — Cut the driver-side ConfigDb connection — **CODE-COMPLETE 2026-07-23 (Tasks 07 + 1b; Task 9 table-drop deferred; Task 10 rig + Task 11 live gate remain)**
**Scope (design §6.1 audit table):** re-home `EfAlarmConditionStateStore` to LocalDb (pair-local
state, same journey as the Phase-2 alarm S&F buffer); **resolve the `DbHealthProbeActor`
question** — driver nodes have no DB to probe, and DB health currently feeds ServiceLevel tiering,
@@ -176,6 +177,27 @@ transports) — this is a client-visible ServiceLevel semantics change and must
design) and re-source it; registration cleanup in `ServiceCollectionExtensions`; driver-role
`Program.cs` branch registers no EF ConfigDb context at all (mirror ScadaBridge's central-only
`AddConfigurationDatabase`).
**Shipped (branch `feat/mesh-phase4`, Tasks 07 + 1b):** `Program.cs` gates `AddOtOpcUaConfigDb` +
the ConfigDb health check on `hasAdmin`, so a driver-only node registers no ConfigDb connection
string at all; `ConfigSourceOptionsValidator` fails host start if a driver-only node's
`ConfigSource:Mode` is left `Direct` (fused `admin,driver` may stay `Direct`); the DB-health
question resolved as **retire, don't replace**`DbHealthProbeActor` is simply not spawned when no
ConfigDb is present, and `OpcUaPublishActor` runs a `dbLess` mode feeding `DbReachable=true`
constant with staleness from the redundancy-snapshot age alone (documented as the client-visible
change in `docs/Redundancy.md`); `LocalDbAlarmConditionStateStore` (replicated
`alarm_condition_state` table) replaces `EfAlarmConditionStateStore` on every driver-role node;
`DriverHostActor` no longer writes `NodeDeploymentState` on a driver-only node (central's
`ConfigPublishCoordinator.PersistNodeAck` was already the ack system of record); driver-only nodes
register `NullLdapGroupRoleMappingService` so LDAP group→role falls back to the
`Security:Ldap:GroupToRole` appsettings baseline (DB grants were never delivered to driver nodes via
config anyway). Docs updated: `docs/Redundancy.md`, `docs/Configuration.md`, `CLAUDE.md`.
**Deferred:** Task 9 — dropping the now-unused `ScriptedAlarmState` ConfigDb table (a follow-up, not
a blocker; the table is simply unread).
**Remaining (Tasks 1011):** rig config for a driver-only node with no `ConfigDb` connection string,
and the live gate itself — a driver pair runs a full deploy + alarm + historian cycle with no
ConfigDb configured at all, confirming ServiceLevel 240/250 while central SQL is down and
grep-level proof no driver-branch service can resolve the ConfigDb context. **Not yet run — treat
Phase 4 as code-complete, not verified, until this lands.**
**Exit gate:** its own live gate — a driver pair runs a full deploy + alarm + historian cycle with
**no ConfigDb connection string configured at all**; grep-level proof no driver-branch service can
resolve the ConfigDb context.
@@ -243,8 +265,8 @@ resource sizing on the site VMs should be checked once both products run the ful
| Prereq: selfform-fallback + manual-failover plan | **DONE 2026-07-22**, merged `a78425ea`. Shipped *not* as the planned `SelfFormAfter` watchdog — the live gate caught that islanding a manually-failed-over node — but as self-first seed ordering + `AkkaClusterOptionsValidator`. Phase 6's seed-ordering scope is therefore already discharged. |
| 1 ClusterNode columns + DB ack set | **DONE 2026-07-22**, merged `7654f24d`. Deploy-seal semantics changed as flagged, but the escape hatch is **`ClusterNode.MaintenanceMode`**, not `Enabled=0` — the live gate found `Enabled=0` is rejected by `DraftValidator.ValidateClusterTopology` on any 2-node pair. Gate record: `2026-07-22-mesh-phase1-live-gate.md`. |
| 2 ClusterClient transport | **DONE 2026-07-22** (live gate PASSED, shipped dark) — shipped dark (`MeshTransport:Mode=Dps` default), both comm actors registered in both modes. Corrections: `SendToAll` not `Send`, one fleet-wide client not one per cluster, `/user/node-communication` not `/user/cluster-communication`. The gate's "Ask timing out" item does not exist to test — no cross-boundary Ask in this phase; see the phase section. |
| 3 fetch-and-cache | **CODE-COMPLETE 2026-07-23** (Tasks 08 merged to the phase branch, all green + sabotage-checked; live gate Task 10 pending). gRPC fetch RPC + shared node key; dark switch `ConfigSource:Mode` (Direct default). Rig flip: `OTOPCUA_CONFIG_MODE=FetchAndCache` on the site nodes. See `2026-07-22-mesh-phase3-config-fetch-and-cache.md`. |
| 4 cut driver ConfigDb | not started |
| 3 fetch-and-cache | **DONE 2026-07-23**, merged `d01b0695`, live gate PASSED, pushed to origin (scadaproj umbrella index updated + pushed `b5e7bc8`). gRPC fetch RPC + shared node key; dark switch `ConfigSource:Mode` (Direct default). Rig flip: `OTOPCUA_CONFIG_MODE=FetchAndCache` on the site nodes. See `2026-07-22-mesh-phase3-config-fetch-and-cache.md`. |
| 4 cut driver ConfigDb | **CODE-COMPLETE 2026-07-23** on `feat/mesh-phase4` (Tasks 07 + 1b done; Task 9 table-drop deferred; Task 10 rig + Task 11 live gate remain — not yet run). ConfigDb admin-only; `DbHealthProbeActor` not spawned driver-only (client-visible ServiceLevel 240/250 with central down); alarm condition state in LocalDb; central persists acks; driver-only LDAP maps from appsettings only. See `2026-07-23-mesh-phase4-cut-driver-configdb.md`. |
| 5 gRPC telemetry | not started |
| 6 mesh partition + co-location | not started |
| 7 drill + live gates | not started |
@@ -76,14 +76,16 @@
{
"id": 7,
"subject": "Task 7: Registration cleanup + full-graph driver-only DI sweep",
"status": "pending",
"status": "completed",
"blockedBy": [
"1b",
2,
3,
4,
6
]
],
"commit": "6f21213f",
"result": "12/12; sweep verified clean; ordering+lazy-resolve pins APPROVED (red-check confirmed load-bearing)"
},
{
"id": 8,
@@ -96,10 +98,11 @@
{
"id": 9,
"subject": "Task 9: Drop dead ConfigDb ScriptedAlarmState table + retire EfAlarmConditionStateStore",
"status": "pending",
"status": "deferred",
"blockedBy": [
6
]
],
"result": "DEFERRED to follow-up (plan-sanctioned): EF store is inert on all real nodes (test-harness fallback only); table-drop migration is irreversible. Not needed for the ConfigDb-cut goal. Tracked in program doc."
},
{
"id": 10,