cd7135570c
MeshPaths.CentralCommunication / .NodeCommunication (the "/user/..." full-path constants) had
ZERO production consumers after the ClusterClient deletion: both gRPC services resolve their
comm actor through ActorRegistry by typed key, never by ActorSelection over a path. Only the
path-probe test still read them, while the class doc went on calling them "the wire contract
of the central↔node boundary" — a claim that stopped being true the moment nothing addressed
an actor across a process boundary by name.
- The two full-path constants are deleted; the two Name constants remain.
- MeshPaths' doc now says what it is (the local actor-naming convention the registry-keyed
services are spawned under) and records what it stopped being, so the next reader does not
re-derive a contract from the file's history.
- MeshCommActorPathTests composes "/user/" + Name itself — the absolute path is that test's
own addressing detail now, not something the product declares. Renamed
Both_mesh_comm_actors_exist_at_their_contract_paths ->
..._are_spawned_under_their_declared_names, because what it still pins is that the two
registration sites actually ran on a real booted node; a rename is now a compile error, so
"contract" was the wrong word for what would break.
- NodeCommunicationActor's summary no longer cross-references the deleted constant or calls
the name an address. CentralCommunicationActor was checked and makes no such claim.
117 lines
5.8 KiB
C#
117 lines
5.8 KiB
C#
using Akka.Actor;
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Node-side end of the central↔node command boundary (per-cluster mesh Phase 2). This node's
|
|
/// <c>NodeCommandService</c> gRPC ingress hands it every decoded command, resolving it from
|
|
/// <c>ActorRegistry</c> by <c>NodeCommunicationActorKey</c> — the actor's name
|
|
/// (<see cref="MeshPaths.NodeCommunicationName"/>) is a local convention, not an address anything
|
|
/// off-process uses.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Registered per node, NOT as a cluster singleton</b> — central dials each node's own
|
|
/// command endpoint, so every node needs its own live comm actor.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Inbound commands are re-emitted on the node's <see cref="EventStream"/>, not on their
|
|
/// DistributedPubSub topic.</b> DPS is mesh-wide, and central delivers one copy 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: <c>ScriptedAlarmHostActor</c> is a <i>child of</i>
|
|
/// <c>DriverHostActor</c> and has no registry key, so there is no ref to hand in at wiring
|
|
/// time.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Outbound is <see cref="ApplyAck"/> only.</b> 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.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class NodeCommunicationActor : ReceiveActor
|
|
{
|
|
private readonly IApplyAckSender? _ackSender;
|
|
private readonly ILoggingAdapter _log = Context.GetLogger();
|
|
|
|
/// <summary>Creates the props for this actor.</summary>
|
|
/// <param name="ackSender">
|
|
/// The gRPC ack transport under <c>MeshTransport:Mode = Grpc</c>, or <see langword="null"/>
|
|
/// under <c>Dps</c>, where acks still go out over DistributedPubSub from
|
|
/// <c>DriverHostActor</c> and this actor never sees them.
|
|
/// </param>
|
|
/// <returns>The props.</returns>
|
|
public static Props Props(IApplyAckSender? ackSender = null) =>
|
|
Akka.Actor.Props.Create(() => new NodeCommunicationActor(ackSender));
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="NodeCommunicationActor"/> class.</summary>
|
|
/// <param name="ackSender">The gRPC ack transport, or <see langword="null"/>.</param>
|
|
public NodeCommunicationActor(IApplyAckSender? ackSender = null)
|
|
{
|
|
_ackSender = ackSender;
|
|
|
|
// 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<DispatchDeployment>(PublishLocally);
|
|
Receive<RestartDriver>(PublishLocally);
|
|
Receive<ReconnectDriver>(PublishLocally);
|
|
Receive<AlarmCommand>(PublishLocally);
|
|
|
|
// Outbound to central.
|
|
Receive<ApplyAck>(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 (_ackSender is not null)
|
|
{
|
|
// Fire-and-forget by construction. The sender never throws and never retries (see
|
|
// IApplyAckSender), so there is nothing to await and nothing to pipe back: awaiting would
|
|
// stall this mailbox behind a network call to central, and an Ask would invent a
|
|
// request/response the one-way contract does not have.
|
|
try
|
|
{
|
|
_ = _ackSender.SendAsync(ack);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// "Never throws" is a contract on a public seam, which makes it documentation until
|
|
// something enforces it. An implementation that threw synchronously — a guard clause
|
|
// ahead of the async state machine is the easy way to do it by accident — would fault
|
|
// this Receive, restart the actor, and take the node's whole inbound command path down
|
|
// with it. One dropped ack is a deployment that times out; a restarting comm actor is
|
|
// every command to this node going missing.
|
|
_log.Error(
|
|
ex,
|
|
"The ApplyAck sender threw synchronously for {DeploymentId}; dropping the ack. "
|
|
+ "IApplyAckSender must never throw — this is a defect in the registered sender.",
|
|
ack.DeploymentId);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
_log.Warning(
|
|
"No central ack transport — 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:CentralGrpcEndpoints, and that MeshTransport:Mode is Grpc on a node "
|
|
+ "whose central lives in another mesh",
|
|
ack.DeploymentId);
|
|
}
|
|
}
|