From af3b0c314648b715315fc805687533cbdbb15bb3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 6 Aug 2026 15:49:42 -0400 Subject: [PATCH] =?UTF-8?q?refactor(mesh):=20step=206=20=E2=80=94=20delete?= =?UTF-8?q?=20the=20ClusterClient=20mode=20value;=20split=20topology=20now?= =?UTF-8?q?=20requires=20Grpc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MeshTransportOptions.ModeClusterClient is gone. MeshTransportOptionsValidator accepts Dps or Grpc and nothing else, so a node left carrying the string "ClusterClient" is now REFUSED at boot naming MeshTransport:Mode and both remaining values — deliberately fail-closed rather than falling back to Dps, which on a split-topology node would deliver nothing at all. A new fact pins exactly that. SplitTopologyTransportValidator tightens from an allow-list of two to Grpc only, as its own comment said it would once ClusterClient was deleted. Dps on a cluster-role node stays refused; the rule is still an explicit allow-list, so a typo'd mode fails too. Tests: the two ClusterClient-mode facts in MeshTransportOptionsValidatorTests are deleted (their Dps siblings already assert the same rule for the one remaining non-Grpc mode), the SplitTopology fixtures move to Grpc, and the three-mode Theories in ControlPlaneStarterOrderTests / CentralCommunicationActorTests become two-mode. --- .../MeshTransportOptions.cs | 38 ++++++++++--------- .../MeshTransportOptionsValidator.cs | 15 +++++--- .../SplitTopologyTransportValidator.cs | 17 ++++----- .../MeshTransportOptionsValidatorTests.cs | 38 ++++++++----------- .../SplitTopologyTransportValidatorTests.cs | 33 ++++++++-------- .../CentralCommunicationActorTests.cs | 9 ++--- .../ControlPlaneStarterOrderTests.cs | 6 +-- 7 files changed, 74 insertions(+), 82 deletions(-) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/MeshTransportOptions.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/MeshTransportOptions.cs index 7fa62de4..33593bdc 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/MeshTransportOptions.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/MeshTransportOptions.cs @@ -6,9 +6,11 @@ namespace ZB.MOM.WW.OtOpcUa.Cluster; /// /// /// -/// is a deliberate dark switch: the transports live in the tree -/// side by side so the docker-dev rig can run the same deployment over each and compare, and -/// so a bad rollout rolls back with a config change rather than a revert. +/// selects between a pair-local transport and a cross-mesh +/// one. is the default and cannot cross an application-Cluster +/// boundary; is the only mode that can, and +/// requires it on any node carrying a +/// cluster-{ClusterId} role. /// /// /// The DistributedPubSub branch is retained, not scheduled for deletion. An earlier @@ -16,15 +18,15 @@ namespace ZB.MOM.WW.OtOpcUa.Cluster; /// deliberately reversed — see docs/Configuration.md § "The compiled transport-mode /// defaults were deliberately NOT flipped". DPS remains the correct and default transport for /// a single-mesh topology, which every integration test and every profile that leaves this -/// key unset still runs. What is being replaced is , by -/// . +/// key unset still runs. /// /// /// Discovery is deliberately asymmetric, matching the sister project. Central /// discovers nodes from the database — enabled, non-maintenance ClusterNode /// rows, rebuilt every . Nodes discover central from -/// this section: static, restart to change. The asymmetry follows the rate of change — -/// operators add and retire nodes, but central's own address is part of the deployment. +/// this section (): static, restart to change. The +/// asymmetry follows the rate of change — operators add and retire nodes, but central's own +/// address is part of the deployment. /// /// public sealed class MeshTransportOptions @@ -32,26 +34,26 @@ public sealed class MeshTransportOptions /// Configuration section name. public const string SectionName = "MeshTransport"; - /// Every command stays on DistributedPubSub — the pre-Phase-2 behaviour. + /// + /// Every command stays on DistributedPubSub — pair-local, and the default. Cannot cross an + /// application-Cluster boundary. + /// public const string ModeDps = "Dps"; - /// Commands cross the ClusterClient receptionist boundary. - public const string ModeClusterClient = "ClusterClient"; - /// /// Commands cross the mesh boundary as unary gRPC calls on the mesh_transport.v1 /// contract — central dials each node's NodeCommandService, a node posts its - /// deployment ack to central's CentralCommandService. The replacement for - /// ; unlike it, the command plane is authenticated - /// (). + /// deployment ack to central's CentralCommandService. The command plane is + /// authenticated per application Cluster (). /// public const string ModeGrpc = "Grpc"; /// - /// (default), or - /// . Any other value fails the host at startup rather than silently - /// falling back — a transport that is none of these delivers nothing, and the symptom would - /// be commands that vanish. + /// (default) or . Any other value fails the host + /// at startup rather than silently falling back — a transport that is neither delivers + /// nothing, and the symptom would be commands that vanish. That includes the retired + /// ClusterClient value: a node still carrying it is refused at boot rather than + /// quietly demoted to DPS, which on a split-topology node would deliver nothing at all. /// public string Mode { get; set; } = ModeDps; diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/MeshTransportOptionsValidator.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/MeshTransportOptionsValidator.cs index 0bd78471..65223102 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/MeshTransportOptionsValidator.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/MeshTransportOptionsValidator.cs @@ -40,16 +40,18 @@ public sealed class MeshTransportOptionsValidator : IValidateOptions(); var isDps = string.Equals(options.Mode, MeshTransportOptions.ModeDps, StringComparison.OrdinalIgnoreCase); - var isClusterClient = string.Equals( - options.Mode, MeshTransportOptions.ModeClusterClient, StringComparison.OrdinalIgnoreCase); var isGrpc = string.Equals(options.Mode, MeshTransportOptions.ModeGrpc, StringComparison.OrdinalIgnoreCase); - if (!isDps && !isClusterClient && !isGrpc) + // An unrecognised mode is refused, never defaulted. The retired 'ClusterClient' value lands + // here: a node left carrying it fails to start with the two remaining values named, rather + // than silently falling back to Dps — which on a split-topology node cannot cross the mesh + // boundary and would deliver nothing at all. + if (!isDps && !isGrpc) { errors.Add( $"{MeshTransportOptions.SectionName}:{nameof(MeshTransportOptions.Mode)} is " - + $"'{options.Mode}'. Expected '{MeshTransportOptions.ModeDps}', " - + $"'{MeshTransportOptions.ModeClusterClient}' or '{MeshTransportOptions.ModeGrpc}'."); + + $"'{options.Mode}'. Expected '{MeshTransportOptions.ModeDps}' or " + + $"'{MeshTransportOptions.ModeGrpc}'."); } if (options.ContactRefreshSeconds <= 0) @@ -64,6 +66,7 @@ public sealed class MeshTransportOptionsValidator : IValidateOptions