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.
81 lines
4.3 KiB
C#
81 lines
4.3 KiB
C#
using Akka.Actor;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Pins that both mesh comm actors are actually <b>spawned</b>, under their declared names, on a
|
|
/// really booted host.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>What this pins is existence, not a wire contract.</b> Since the cross-mesh transport
|
|
/// became gRPC, both services resolve their comm actor through <c>ActorRegistry</c> rather
|
|
/// than by path, so a rename is a compile error rather than a silent drop and
|
|
/// <see cref="MeshPaths"/> is only a local naming convention. What is still unpinned by
|
|
/// anything else is whether the two registration sites
|
|
/// (<c>WithOtOpcUaControlPlaneSingletons</c> / <c>WithOtOpcUaRuntimeActors</c>) actually run
|
|
/// and spawn these actors on a real node — and if either did not, every inbound call would be
|
|
/// refused as <c>Unavailable</c> ("the node is still starting"), which reads as a boot-window
|
|
/// race rather than as an actor that will never appear.
|
|
/// </para>
|
|
/// <para>
|
|
/// Booting the real host is the point. A test that spawned the actors itself would pin the
|
|
/// constant against itself and prove nothing about that wiring.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class MeshCommActorPathTests
|
|
{
|
|
/// <summary>
|
|
/// Builds the <c>ActorSelection</c> string for a top-level actor name. Composed here rather
|
|
/// than held as a constant: the full path is a local addressing detail of this test, not
|
|
/// something the product declares — see <see cref="MeshPaths"/>.
|
|
/// </summary>
|
|
/// <param name="actorName">The declared actor name.</param>
|
|
/// <returns>The absolute user-guardian path.</returns>
|
|
private static string UserPath(string actorName) => "/user/" + actorName;
|
|
|
|
private static async Task AssertActorExistsAsync(ActorSystem system, string actorName)
|
|
{
|
|
var path = UserPath(actorName);
|
|
var identity = await system.ActorSelection(path)
|
|
.Ask<ActorIdentity>(new Identify(path), TimeSpan.FromSeconds(10));
|
|
|
|
identity.Subject.ShouldNotBeNull(
|
|
$"no actor exists at {path}; the registration that spawns it did not run, and every "
|
|
+ "inbound mesh call to this node would be refused as a misleading readiness rejection");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Both_mesh_comm_actors_are_spawned_under_their_declared_names()
|
|
{
|
|
await using var harness = await TwoNodeClusterHarness.StartAsync();
|
|
|
|
// Harness nodes carry admin+driver, so BOTH comm actors live on each of them.
|
|
await AssertActorExistsAsync(harness.NodeASystem, MeshPaths.CentralCommunicationName);
|
|
await AssertActorExistsAsync(harness.NodeASystem, MeshPaths.NodeCommunicationName);
|
|
await AssertActorExistsAsync(harness.NodeBSystem, MeshPaths.CentralCommunicationName);
|
|
await AssertActorExistsAsync(harness.NodeBSystem, MeshPaths.NodeCommunicationName);
|
|
}
|
|
|
|
// NOT TESTED HERE: that the comm actors are registered per node rather than as cluster
|
|
// singletons. Two candidate discriminators were tried and BOTH were invalid:
|
|
//
|
|
// * "the path resolves on both nodes" — a ClusterSingletonManager is also created on every
|
|
// node in the role, so this passes under either shape.
|
|
// * "no /singleton child exists" — measured against the KNOWN singleton /user/config-publish,
|
|
// that child is null there too, so Akka.Hosting does not lay singletons out this way and
|
|
// the assertion proves nothing.
|
|
//
|
|
// A sabotage that re-registered CentralCommunicationActor via WithSingleton left both green,
|
|
// which is what exposed them. Rather than ship an assertion I could not make fail, the property
|
|
// is left to live-gate step 8 (stop the OLDEST central; the survivor's comm actor must still
|
|
// receive a node ack, which a singleton hosted on the stopped node cannot).
|
|
//
|
|
// NOT MeshGrpcBoundaryTests: that class proves a command only arrives if the target is
|
|
// registered under its registry key, which is necessary but not discriminating — with one node
|
|
// per side, per-node and singleton registration look identical.
|
|
}
|