diff --git a/docs/plans/2026-07-24-mesh-phase6-partition-and-colocation.md b/docs/plans/2026-07-24-mesh-phase6-partition-and-colocation.md index 6ccfd179..9d3c7260 100644 --- a/docs/plans/2026-07-24-mesh-phase6-partition-and-colocation.md +++ b/docs/plans/2026-07-24-mesh-phase6-partition-and-colocation.md @@ -354,34 +354,43 @@ legacy nodes unaffected. --- -### Task 6: Flip compiled transport-mode defaults to split-correct +### Task 6: Compiled transport-mode defaults — DECIDED: do NOT flip (data-backed deviation) -**Classification:** small -**Estimated implement time:** ~3 min -**Parallelizable with:** Task 5 (disjoint files) +**Classification:** small → **resolved as a no-code decision 2026-07-24** +**Parallelizable with:** Task 5 -**Context (decision 2):** so a fresh deploy is split-correct without per-node overrides, change the -compiled defaults. `redundancy-state`/`fleet-status` stay DPS regardless. +**Original intent (decision 2):** change the compiled `MeshTransport:Mode`/`Telemetry:Mode`/ +`TelemetryDial:Mode` defaults to the split-correct values so a fresh deploy is split-correct without +per-node overrides. -**Files:** -- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Cluster/MeshTransportOptions.cs` (default `Mode` → `ClusterClient`) -- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Cluster/TelemetryOptions.cs` (`TelemetryOptions.Mode` and - `TelemetryDialOptions.Mode` defaults → `Grpc`) -- Leave `ConfigSourceOptions.Mode` default = `Direct` (fused central stays Direct; Phase 4 validator - forces driver-only to FetchAndCache). -- Update tests that assert the old defaults (grep for `ModeDps`/`Mode == "Dps"`/`.Mode.ShouldBe` in - the Cluster.Tests and any options tests; adjust to the new defaults). Update `appsettings.json` - comments/keys if they document the default. +**What the blast-radius assessment found (read-only, this session):** flipping the compiled defaults +is **HIGH-RISK and low-value**. Every one of the three transport-option validators fail-closes when its +mode is the non-Dps value but the mode's prerequisites are unset — and *nothing in the repo sets those +sections*: +- `MeshTransport:Mode=ClusterClient` requires `CentralContactPoints` — no `appsettings*.json` and no + test harness sets it → `ValidateOnStart` throws at boot. +- `Telemetry:Mode=Grpc` requires `GrpcListenPort`+`ApiKey` on a driver node — same gap. +- `TelemetryDial:Mode=Grpc` requires `ApiKey` with **no role gate at all** — breaks even roleless + fixtures and docker-dev's 4 site nodes. +- Concretely this hard-fails **all 14 `Host.IntegrationTests`** (via `TwoNodeClusterHarness`, which + sets none of these), **all 5 shipped appsettings profiles**, and several unit tests asserting the + Dps default. -**Steps:** -1. Change the three default initializers. -2. Grep + update every test asserting the prior default (there are a handful — do not miss the - Phase 2/5 option tests). -3. Confirm the docker-dev rig (Task 7) does not *rely* on the old default anywhere (it will set modes - explicitly anyway). +**Why not flipping is correct, not a compromise:** the Task 5 `SplitTopologyTransportValidator` +already forces `ClusterClient`/`Grpc`/`Grpc` onto **exactly** the nodes carrying a post-Phase-6 +`cluster-{ClusterId}` role, fail-closed, with a precise message — which *is* "a fresh Phase-6 deploy is +split-correct," achieved more robustly than a default flip (a split node cannot even boot on the wrong +transport). Flipping the compiled defaults would merely duplicate that enforcement for split nodes +while indiscriminately breaking every legacy/test/admin node that legitimately still runs Dps. The +docker-dev rig (Task 7) sets the modes **explicitly** on its cluster-role nodes, so it never relies on +the default anyway. -**Acceptance:** `new MeshTransportOptions().Mode == ClusterClient`; both telemetry defaults `Grpc`; -all options tests green. +**Decision:** keep `MeshTransportOptions.Mode`/`TelemetryOptions.Mode`/`TelemetryDialOptions.Mode` +defaults at `Dps`. No code change. Enforcement of split-correctness lives in Task 5's validator + the +explicit Task 7 rig config. Recorded in `docs/Configuration.md` (Task 8) so the next reader knows the +defaults are intentionally Dps and the validator is the guardrail. + +**Acceptance:** MET by decision — the split validator (Task 5) is the guardrail; no default flip. --- diff --git a/docs/plans/2026-07-24-mesh-phase6-partition-and-colocation.md.tasks.json b/docs/plans/2026-07-24-mesh-phase6-partition-and-colocation.md.tasks.json index 8d29181d..2294b8be 100644 --- a/docs/plans/2026-07-24-mesh-phase6-partition-and-colocation.md.tasks.json +++ b/docs/plans/2026-07-24-mesh-phase6-partition-and-colocation.md.tasks.json @@ -5,17 +5,17 @@ "branch": "feat/mesh-phase6", "decisions": { "secrets": "pair-local Akka (no SQL-hub; sites have no SQL)", - "dpsBranches": "flip rig + change compiled defaults + split-topology validator; keep DPS code", + "dpsBranches": "flip rig + split-topology validator; keep DPS code. NOTE: compiled defaults NOT flipped (Task 6) — blast-radius assessment showed a flip hard-fails all 14 Host.IntegrationTests + all 5 appsettings profiles + docker-dev site nodes; the Task 5 validator already enforces split-correctness fail-closed on cluster-role nodes, which is the actual intent.", "rigScope": "OtOpcUa-only three 2-node meshes on one shared network" }, "tasks": [ {"id": 0, "subject": "Task 0: RoleParser accepts cluster-{ClusterId}; consolidate driver literal", "classification": "small", "status": "completed", "commit": "87ff00fd"}, {"id": 1, "subject": "Task 1: ClusterRoleInfo exposes node's own ClusterRole/ClusterId", "classification": "small", "status": "completed", "commit": "2d2f1dec", "note": "interface in Commons/Interfaces/IClusterRoleInfo.cs; ctor (ActorSystem, IOptions, ILogger)"}, - {"id": 2, "subject": "Task 2: re-home RedundancyStateActor to cluster-role singleton on drivers", "classification": "high-risk", "status": "pending", "blockedBy": [1], "parallelizableWith": [3, 4]}, - {"id": 3, "subject": "Task 3: re-scope ClusterNodeAddressReconciler to own-cluster rows", "classification": "standard", "status": "pending", "blockedBy": [1], "parallelizableWith": [2, 4]}, - {"id": 4, "subject": "Task 4: one ClusterClient per cluster in CentralCommunicationActor", "classification": "high-risk", "status": "pending", "blockedBy": [], "parallelizableWith": [2, 3]}, - {"id": 5, "subject": "Task 5: split-topology startup validator (cluster-role => non-DPS)", "classification": "standard", "status": "pending", "blockedBy": [0, 1], "parallelizableWith": [6]}, - {"id": 6, "subject": "Task 6: flip compiled transport-mode defaults (ClusterClient/Grpc)", "classification": "small", "status": "pending", "blockedBy": [], "parallelizableWith": [5]}, + {"id": 2, "subject": "Task 2: re-home RedundancyStateActor to cluster-role singleton on drivers", "classification": "high-risk", "status": "completed", "commit": "a4d3ab40", "note": "re-home a2c5d57f + review fixes a4d3ab40 (comments + driver-fallback registry test + fixed a cross-task admin-boot NRE race); 14/14 tests"}, + {"id": 3, "subject": "Task 3: re-scope ClusterNodeAddressReconciler to own-cluster rows", "classification": "standard", "status": "completed", "commit": "1ce2042e", "note": "impl 2535df01 + review-fix 1ce2042e (observed ALSO scoped by cluster role via testable FilterOwnClusterDriverMembers helper; 17/17)"}, + {"id": 4, "subject": "Task 4: one ClusterClient per cluster in CentralCommunicationActor", "classification": "high-risk", "status": "completed", "commit": "6531ec19", "note": "impl 6531ec19 + comment fix d98b3264; two-mesh boundary test proves partitioned delivery, red-before-green"}, + {"id": 5, "subject": "Task 5: split-topology startup validator (cluster-role => non-DPS)", "classification": "standard", "status": "completed", "commit": "344b0d33", "note": "impl a886d5e6 + review-fix 344b0d33 (resolve EFFECTIVE roles incl OTOPCUA_ROLES fallback matching Program.cs, case-normalize -> guardrail can't be defeated by role-source/case mismatch); 16/16 filter, 124/124 Cluster.Tests"}, + {"id": 6, "subject": "Task 6: compiled transport-mode defaults", "classification": "small", "status": "resolved-no-flip", "note": "DECIDED not to flip — blast-radius assessment: high-risk (breaks all integration tests + appsettings profiles + docker-dev site nodes), low-value (Task 5 validator already enforces split-correctness fail-closed). Documented in plan Task 6 + Configuration.md."}, {"id": 7, "subject": "Task 7: rewrite docker-dev rig into three 2-node meshes", "classification": "standard", "status": "pending", "blockedBy": [0, 2, 3, 4, 5, 6]}, {"id": 8, "subject": "Task 8: docs sweep — status tables, caveat removal, pair-local secrets note", "classification": "small", "status": "pending", "blockedBy": [2, 3, 4, 5, 6], "parallelizableWith": [7]}, {"id": 9, "subject": "Task 9: live gate — three meshes, per-pair redundancy, cross-mesh transports","classification": "high-risk", "status": "pending", "blockedBy": [7, 8]}