From 07bc1878fca14083080f51f003a0b8f61d624716 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 6 Aug 2026 06:22:44 -0400 Subject: [PATCH] =?UTF-8?q?docs(mesh):=20sweep=20the=20remaining=20doubled?= =?UTF-8?q?-quorum=20comment=20sites=20=E2=80=94=20the=20coordinator=20abs?= =?UTF-8?q?orbs=20duplicates=20idempotently?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Communication/NodeCommunicationActor.cs | 5 +++-- .../Communication/GrpcApplyAckSenderTests.cs | 5 +++-- .../Communication/NodeCommunicationActorTests.cs | 10 ++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Communication/NodeCommunicationActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Communication/NodeCommunicationActor.cs index f8b7d5fd..88702dc8 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Communication/NodeCommunicationActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Communication/NodeCommunicationActor.cs @@ -128,8 +128,9 @@ public sealed class NodeCommunicationActor : ReceiveActor } // Send, not SendToAll: the deploy coordinator is a single singleton, and the central comm - // actor forwards to its proxy. SendToAll would deliver one ack per central node, and the - // coordinator would count the same node twice. + // actor forwards to its proxy. Its NodeId-keyed upsert absorbs a duplicate idempotently, + // so single-target is defence in depth and a bound on wasted sends — mirroring the + // corrected Grpc-branch rationale above. _centralClient.Tell(new ClusterClient.Send(MeshPaths.CentralCommunication, ack)); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Communication/GrpcApplyAckSenderTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Communication/GrpcApplyAckSenderTests.cs index 7974ae99..bd6603f8 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Communication/GrpcApplyAckSenderTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Communication/GrpcApplyAckSenderTests.cs @@ -22,8 +22,9 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Communication; /// The load-bearing fact is that ONE endpoint is dialled on success. This replaces an /// Akka ClusterClient.Send, which is single-target at-most-once — not a /// SendToAll. Both central nodes route an ack to the same -/// ConfigPublishCoordinator singleton proxy, so posting to both would have the -/// coordinator count this node twice and could seal a deployment on a doubled quorum. +/// ConfigPublishCoordinator singleton proxy, whose NodeId-keyed upsert absorbs a +/// duplicate idempotently — so the single-endpoint rule is defence in depth and a bound +/// on wasted dials, not what protects the seal. /// /// /// The failure side matters just as much: the receptionist dropped an undeliverable ack in diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Communication/NodeCommunicationActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Communication/NodeCommunicationActorTests.cs index fbbe0003..473d2d02 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Communication/NodeCommunicationActorTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Communication/NodeCommunicationActorTests.cs @@ -94,8 +94,9 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase public void ApplyAck_goes_to_central_as_a_Send_not_a_SendToAll() { // Send, not SendToAll: the deploy coordinator is one singleton behind the central comm - // actor's proxy. SendToAll would deliver one ack per central node and the coordinator would - // count this node twice. + // actor's proxy. The coordinator's NodeId-keyed upsert absorbs a duplicate idempotently, + // so the single-target rule is defence in depth and a bound on wasted work, not a + // correctness requirement. var client = CreateTestProbe(); var actor = Sys.ActorOf(NodeCommunicationActor.Props(client.Ref)); @@ -161,8 +162,9 @@ public class NodeCommunicationActorTests : RuntimeActorTestBase public void A_sender_wins_over_a_cluster_client_and_the_ack_is_not_sent_twice() { // Defence in depth. The registration arm builds exactly one transport — under Grpc no - // ClusterClient is created at all — but if both were ever present, posting over both would - // deliver two acks for one apply and the coordinator would count this node twice. + // ClusterClient is created at all — and if both were ever present, posting over both would + // deliver two acks for one apply. The coordinator would absorb the duplicate (NodeId-keyed + // upsert), so this pins tidiness and bounded work, not seal correctness. var client = CreateTestProbe(); var sender = new RecordingAckSender(); var actor = Sys.ActorOf(NodeCommunicationActor.Props(client.Ref, sender));