ab5b958d47
WithOtOpcUaRuntimeActors no longer builds a ClusterClient from MeshTransport:CentralContactPoints, and NodeCommunicationActor.Props takes only the gRPC IApplyAckSender. The ackRouter gate narrows from `useClusterClient || useGrpc` to `useGrpc`, and the CentralClusterClientName constant is gone. NodeCommunicationActor's no-transport warning now names MeshTransport:CentralGrpcEndpoints and MeshTransport:Mode only — the old text pointed an operator at a key that no longer exists. Ripples in this step: the two ClusterClient-shaped facts in NodeCommunicationActorTests (Send-not-SendToAll, and sender-wins-over-a-client) are deleted — the first has no subject left, the second guarded a both-transports-present state that is now unconstructible. The Runtime ServiceCollectionExtensions fact keeps its ackRouter pin and drops its "no ClusterClient actor exists" half for the same reason.
162 lines
7.3 KiB
C#
162 lines
7.3 KiB
C#
using Akka.Actor;
|
|
using Akka.TestKit.Xunit2;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Communication;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
|
using Serilog;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
|
|
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Communication;
|
|
|
|
/// <summary>
|
|
/// Proves the Phase 2 node-side leg joins up: a command handed to
|
|
/// <see cref="NodeCommunicationActor"/> reaches the <b>real</b> <see cref="DriverHostActor"/>,
|
|
/// not just a test probe.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The unit tests either side of this seam both pass with the wiring broken.
|
|
/// <c>NodeCommunicationActorTests</c> asserts a probe subscribed to the EventStream receives
|
|
/// the message; the driver-host tests drive the actor by direct <c>Tell</c>. Neither notices
|
|
/// if <c>DriverHostActor.PreStart</c> never subscribes. That gap is why this file exists.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Ordering matters, and it bit this test first time out.</b> <c>ActorOf</c> returns
|
|
/// before <c>PreStart</c> has run, and an <c>EventStream.Publish</c> with no subscriber is
|
|
/// dropped silently — there is no buffering and no dead-letter. Relaying immediately after
|
|
/// spawning therefore fails intermittently. The warm-up below is not ceremony: it is the
|
|
/// only available proof that <c>PreStart</c> has completed. (In production the driver host
|
|
/// is spawned at startup, long before any deployment, so the same race does not arise.)
|
|
/// </para>
|
|
/// </remarks>
|
|
public class EventStreamCommandDeliveryTests : RuntimeActorTestBase
|
|
{
|
|
private static DispatchDeployment NewDispatch() => new(
|
|
new DeploymentId(Guid.NewGuid()),
|
|
new RevisionHash("rev-does-not-exist"),
|
|
new CorrelationId(Guid.NewGuid()));
|
|
|
|
[Fact]
|
|
public void A_dispatch_relayed_by_the_comm_actor_reaches_the_real_driver_host()
|
|
{
|
|
var ackRouter = CreateTestProbe();
|
|
var driverHost = Sys.ActorOf(
|
|
DriverHostActor.Props(NewInMemoryDbFactory(), new NodeId("127.0.0.1:4053"), ackRouter: ackRouter.Ref),
|
|
"driver-host");
|
|
var comm = Sys.ActorOf(NodeCommunicationActor.Props(), "node-communication");
|
|
|
|
// Warm-up: a direct Tell whose ack proves PreStart has completed, and with it the
|
|
// EventStream subscription. Without this the relay below races the subscription.
|
|
var warmup = NewDispatch();
|
|
driverHost.Tell(warmup);
|
|
ackRouter.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(15)).DeploymentId.ShouldBe(warmup.DeploymentId);
|
|
|
|
// Now the real assertion — relayed exactly as central would send it. The comm actor only
|
|
// publishes node-locally, so the host must be subscribed for this to arrive at all.
|
|
var relayed = NewDispatch();
|
|
comm.Tell(relayed);
|
|
|
|
// The deployment id does not exist, so the host answers Failed. That is the stronger
|
|
// assertion, not a weaker one: an ack at all proves the command was received and handled,
|
|
// and only PreStart's EventStream subscription can have delivered it.
|
|
ackRouter.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(15)).DeploymentId.ShouldBe(relayed.DeploymentId);
|
|
}
|
|
|
|
[Fact]
|
|
public void CONTROL_the_driver_host_acks_a_direct_tell()
|
|
{
|
|
// Falsifiability control. If the test above fails, this one says which half broke: green
|
|
// here means the host is healthy and the EventStream relay is at fault; red here means the
|
|
// host would not have acked either way and the relay is not implicated.
|
|
var ackRouter = CreateTestProbe();
|
|
var driverHost = Sys.ActorOf(
|
|
DriverHostActor.Props(NewInMemoryDbFactory(), new NodeId("127.0.0.1:4053"), ackRouter: ackRouter.Ref),
|
|
"driver-host-control");
|
|
|
|
var dispatch = NewDispatch();
|
|
driverHost.Tell(dispatch);
|
|
|
|
ackRouter.ExpectMsg<ApplyAck>(TimeSpan.FromSeconds(15)).DeploymentId.ShouldBe(dispatch.DeploymentId);
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// The alarm leg of the same wiring proof, in its own ActorSystem because the shared
|
|
/// <see cref="RuntimeActorTestBase"/> harness pins <c>loglevel = WARNING</c> and the only
|
|
/// observable signal <c>ScriptedAlarmHostActor</c> gives for an unowned alarm is a Debug line.
|
|
/// </summary>
|
|
public class AlarmCommandEventStreamDeliveryTests : TestKit
|
|
{
|
|
private static string DebugLevelHocon => @"
|
|
akka {
|
|
loglevel = ""DEBUG""
|
|
extensions = [
|
|
""Akka.Cluster.Tools.PublishSubscribe.DistributedPubSubExtensionProvider, Akka.Cluster.Tools""
|
|
]
|
|
actor {
|
|
provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster""
|
|
}
|
|
remote.dot-netty.tcp {
|
|
hostname = ""127.0.0.1""
|
|
port = 0
|
|
}
|
|
cluster {
|
|
seed-nodes = []
|
|
roles = [""driver""]
|
|
min-nr-of-members = 1
|
|
run-coordinated-shutdown-when-down = off
|
|
}
|
|
}";
|
|
|
|
public AlarmCommandEventStreamDeliveryTests()
|
|
: base(DebugLevelHocon)
|
|
{
|
|
var cluster = Akka.Cluster.Cluster.Get(Sys);
|
|
cluster.Join(cluster.SelfAddress);
|
|
AwaitCondition(
|
|
() => cluster.State.Members.Any(m => m.Status == Akka.Cluster.MemberStatus.Up),
|
|
TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
[Fact]
|
|
public void An_alarm_command_relayed_by_the_comm_actor_reaches_the_real_scripted_alarm_host()
|
|
{
|
|
// Needed for the same reason as the deploy leg: the comm actor's unit test only proves a
|
|
// probe receives it, and the alarm-host tests drive the actor by direct Tell. Neither
|
|
// notices a missing PreStart subscription.
|
|
var upstream = new DependencyMuxTagUpstreamSource();
|
|
var logger = new LoggerConfiguration().CreateLogger();
|
|
var engine = new ScriptedAlarmEngine(
|
|
upstream, new InMemoryAlarmStateStore(), new ScriptLoggerFactory(logger), logger);
|
|
|
|
var host = Sys.ActorOf(
|
|
ScriptedAlarmHostActor.Props(
|
|
CreateTestProbe().Ref, CreateTestProbe().Ref, upstream, engine, new NodeId("node-A")),
|
|
"scripted-alarm-host");
|
|
var comm = Sys.ActorOf(NodeCommunicationActor.Props(), "node-communication");
|
|
|
|
var cmd = new AlarmCommand("alarm-unowned", "Acknowledge", "alice", "c", null);
|
|
|
|
// Warm-up, OUTSIDE the real assertion: a direct Tell whose debug line proves PreStart has
|
|
// completed and the EventStream subscription is registered. An earlier draft did the direct
|
|
// Tell INSIDE the assertion window alongside the relay, which made the test vacuous — the
|
|
// direct Tell alone produced the log the assertion was waiting for.
|
|
EventFilter.Debug(contains: "ignoring AlarmCommand")
|
|
.ExpectOne(TimeSpan.FromSeconds(15), () => host.Tell(cmd));
|
|
|
|
// The real assertion: relayed ONLY through the comm actor, which publishes node-locally.
|
|
// The host owns no alarms, so it logs and stops — still a positive delivery signal, because
|
|
// that line is only reached if the message arrived.
|
|
EventFilter.Debug(contains: "ignoring AlarmCommand")
|
|
.ExpectOne(TimeSpan.FromSeconds(15), () => comm.Tell(cmd));
|
|
}
|
|
}
|