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));
}
}