using Akka.Actor; using Akka.Cluster.Tools.Client; using Akka.Event; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; namespace ZB.MOM.WW.OtOpcUa.Runtime.Communication; /// /// Node-side end of the central↔node command boundary (per-cluster mesh Phase 2), registered /// with the ClusterClientReceptionist at . /// /// /// /// Registered per node, NOT as a cluster singleton — central's ClusterClient rotates /// across contact points and must find a live comm actor at whichever node answers. /// /// /// Inbound commands are re-emitted on the node's , not on their /// DistributedPubSub topic. DPS is mesh-wide, and central SendToAlls to every /// node's comm actor — so a DPS re-publish would deliver N copies of every command to every /// subscriber, where N is the node count. The EventStream is node-local by construction, /// which is exactly the needed semantics. It also avoids a registration handshake with /// actors spawned later: ScriptedAlarmHostActor is a child of /// DriverHostActor and has no registry key, so there is no ref to hand in at wiring /// time. /// /// /// Outbound is only. There is no Ask across this boundary in /// Phase 2 — every migrated command is fire-and-forget, and the deploy ack returns as an /// independent message the coordinator already subscribes for. Nothing here needs sender /// preservation, which is why this actor is far smaller than the sister project's. /// /// public sealed class NodeCommunicationActor : ReceiveActor { private readonly IActorRef? _centralClient; private readonly ILoggingAdapter _log = Context.GetLogger(); /// Creates the props for this actor. /// /// ClusterClient dialling central's receptionist, or under /// MeshTransport:Mode = Dps, where acks still go out over DistributedPubSub from /// DriverHostActor and this actor never sees them. /// /// The props. public static Props Props(IActorRef? centralClient) => Akka.Actor.Props.Create(() => new NodeCommunicationActor(centralClient)); /// Initializes a new instance of the class. /// ClusterClient dialling central, or . public NodeCommunicationActor(IActorRef? centralClient) { _centralClient = centralClient; // Inbound from central. Each type is listed explicitly rather than caught by a ReceiveAny: // an unknown type must dead-letter loudly during development rather than be republished // blind onto the node's EventStream, where it would be silently ignored by every subscriber. Receive(PublishLocally); Receive(PublishLocally); Receive(PublishLocally); Receive(PublishLocally); // Outbound to central. Receive(HandleApplyAck); } private void PublishLocally(object msg) { _log.Debug("Mesh: received {MessageType} from central; publishing node-locally", msg.GetType().Name); Context.System.EventStream.Publish(msg); } private void HandleApplyAck(ApplyAck ack) { if (_centralClient is null) { _log.Warning( "No central ClusterClient — dropping ApplyAck for {DeploymentId}. The ack is not " + "buffered and will not be retried, so the deployment will time out at central " + "naming this node even though it applied successfully. Check " + "MeshTransport:CentralContactPoints", ack.DeploymentId); return; } // 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. _centralClient.Tell(new ClusterClient.Send(MeshPaths.CentralCommunication, ack)); } }