using Akka.Actor; using Akka.Cluster.Tools.Client; using Akka.Configuration; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests; /// /// Asserts the ClusterClient / receptionist / frame-size settings that are actually in /// force on a real built from the shipped base config. /// /// /// Asserting the akka.conf text instead would pass while a competing HOCON fragment /// silently overrode the value — the failure mode recorded at length in /// ServiceCollectionExtensions.BuildDowningHocon and pinned the same way by /// SplitBrainResolverActivationTests. In this codebase the effective value is the only /// one worth asserting. /// public class MeshTransportHoconTests : IDisposable { private readonly ActorSystem _sys; public MeshTransportHoconTests() { var config = ConfigurationFactory .ParseString("akka.remote.dot-netty.tcp.port = 0\nakka.cluster.seed-nodes = []") .WithFallback(HoconLoader.LoadBaseConfig()); _sys = ActorSystem.Create("mesh-hocon-probe", config); } [Fact] public void Cluster_client_buffering_is_disabled() { // Asserted through ClusterClientSettings — the object the client is actually built from — // NOT through Config.GetInt. GetInt returns 0 for a MISSING key, so a config assertion here // passes identically whether the setting is explicitly 0 or absent with Akka's default of // 1000 in force. It was green before this feature existed; that is the whole defect class. // // buffer-size = 0 means "drop now if the receptionist is unknown". Buffering would replay a // DispatchDeployment on reconnect and apply a deployment the coordinator already sealed as // TimedOut, leaving the node running a configuration the database says failed. This is a // behaviour decision, not a tuning knob. ClusterClientSettings.Create(_sys).BufferSize.ShouldBe(0); } [Fact] public void Cluster_client_retries_forever_rather_than_self_stopping() { // reconnect-timeout = off surfaces as a null TimeSpan on the settings object. A central node // down for an hour must not require a driver-node restart to become reachable again. ClusterClientSettings.Create(_sys).ReconnectTimeout.ShouldBeNull(); } [Fact] public void Receptionist_serves_every_member_regardless_of_role() { // An empty role is what makes "registered per node, not as a singleton" work: every member // hosts a receptionist, so ClusterClient contact rotation reaches whichever node answers. // Scoping this to a role would make the boundary reachable through one node only. ClusterReceptionistSettings.Create(_sys).Role.ShouldBeNull(); } [Fact] public void Oversized_frames_are_logged_rather_than_dropped_silently() { // The sister project's most expensive incident. A message over the frame limit is dropped // WITHOUT tearing down the association: heartbeats keep flowing, the node still reports // healthy, and the only symptom is a command that never arrived. log-frame-size-exceeding // converts that silence into a log line naming the offending size. _sys.Settings.Config.GetByteSize("akka.remote.dot-netty.tcp.maximum-frame-size").ShouldBe(128000); _sys.Settings.Config.GetByteSize("akka.remote.dot-netty.tcp.log-frame-size-exceeding").ShouldBe(32000); } [Fact] public void Receptionist_extension_is_registered_at_startup() { // Not auto-started. Without this entry the receptionist materialises only if some code path // happens to call ClusterClientReceptionist.Get(system) first, which makes "is the boundary // listening?" depend on startup ordering rather than on configuration. _sys.Settings.Config.GetStringList("akka.extensions") .ShouldContain(s => s.Contains("ClusterClientReceptionistExtensionProvider")); } [Fact] public void Distributed_pub_sub_extension_survives_alongside_the_receptionist() { // The dark switch keeps both transports live for one phase, and DPS additionally carries // traffic Phase 2 does not touch at all — node-local alarm-command publishes and the // node<->node redundancy-state probe leg. Losing it here would break far more than deploys. _sys.Settings.Config.GetStringList("akka.extensions") .ShouldContain(s => s.Contains("DistributedPubSubExtensionProvider")); } public void Dispose() { _sys.Terminate().Wait(TimeSpan.FromSeconds(5)); GC.SuppressFinalize(this); } }