7b71a6a35d
Phase 2 Task 1. Three changes to the embedded base config, plus a test that reads every one of them back off a RUNNING ActorSystem rather than off the HOCON text -- the settled idiom here (SplitBrainResolverActivationTests), because several fragments compete and the losing one still reads correctly in the file. - Registers the ClusterClientReceptionist extension. It is not auto-started, so without this the boundary only listens if some code path happens to call ClusterClientReceptionist.Get(system) first: "is the boundary up?" would depend on startup ordering rather than on configuration. - buffer-size = 0. This is the one genuine behaviour change and it is NOT what the sister project runs -- they never wrote this section and inherit 1000. Buffering would replay a DispatchDeployment on reconnect and apply a deployment the coordinator already sealed as TimedOut, leaving the node on a configuration the database says failed. - log-frame-size-exceeding = 32000b. An oversized frame is dropped WITHOUT tearing down the association: heartbeats keep flowing, the node reports healthy, and the only symptom is a command that never arrived. Our deploy notify is payload-free, so this canary should stay quiet. reconnect-timeout and receptionist.role restate Akka defaults; they are stated explicitly and pinned because the behaviour is load-bearing, and the comments say which is which. One test was VACUOUS on first write: Config.GetInt returns 0 for a missing key, so the buffering assertion passed before the feature existed -- it could not distinguish "explicitly 0" from "absent, default 1000 in force". Rewritten to assert through ClusterClientSettings/ClusterReceptionistSettings, the objects the client is actually built from. Sabotage-verified after the fix: setting buffer-size back to 1000 reddens exactly that one test. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
101 lines
4.7 KiB
C#
101 lines
4.7 KiB
C#
using Akka.Actor;
|
|
using Akka.Cluster.Tools.Client;
|
|
using Akka.Configuration;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
|
|
|
|
/// <summary>
|
|
/// Asserts the ClusterClient / receptionist / frame-size settings that are actually <b>in
|
|
/// force</b> on a real <see cref="ActorSystem"/> built from the shipped base config.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Asserting the <c>akka.conf</c> text instead would pass while a competing HOCON fragment
|
|
/// silently overrode the value — the failure mode recorded at length in
|
|
/// <c>ServiceCollectionExtensions.BuildDowningHocon</c> and pinned the same way by
|
|
/// <c>SplitBrainResolverActivationTests</c>. In this codebase the effective value is the only
|
|
/// one worth asserting.
|
|
/// </remarks>
|
|
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);
|
|
}
|
|
}
|