Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/Cluster/ActiveNodeSemanticsTests.cs
T

70 lines
3.4 KiB
C#

using Akka.Cluster;
using Xunit;
using ZB.MOM.WW.ScadaBridge.Host.Health;
namespace ZB.MOM.WW.ScadaBridge.IntegrationTests.Cluster;
/// <summary>
/// The exact scenario from arch-review 01 [High]: after the original first node
/// (oldest = singleton host) leaves and rejoins, it comes back as the YOUNGEST
/// member while the peer is now oldest — yet by Akka's address-ordered rule the
/// rejoiner may regain cluster LEADERSHIP. The two definitions permanently
/// diverge, which is why every product active/standby decision must use
/// <see cref="ClusterActivityEvaluator.SelfIsOldest(Cluster, string?)"/> (oldest
/// member = singleton host) and never <c>cluster.State.Leader</c>.
///
/// DEVIATION FROM THE PLAN (documented): the plan drafted this as a hard CRASH of
/// the oldest node. Under two-node <c>keep-oldest</c> (verified empirically in
/// <c>SbrFailoverTests</c>, Akka 1.5.62) a hard crash of the oldest makes the
/// younger survivor down ITSELF — total cluster loss — so the survivor could not
/// be asserted to stay up. A graceful cluster LEAVE of the oldest instead models a
/// rolling restart / redeploy of the first node (the realistic way the original
/// oldest cycles out) and hands oldest-ship to the survivor cleanly, without any
/// unreachable-partition SBR decision. The divergence assertions are identical and
/// leadership-independent either way.
/// </summary>
public class ActiveNodeSemanticsTests
{
[Fact]
public async Task AfterOldestNodeRestarts_SurvivorStaysActive_RejoinerIsStandby()
{
await using var fixture = await TwoNodeClusterFixture.StartAsync();
// Gracefully remove A (oldest); B takes over as oldest. A graceful Leave
// (not a hard crash) is required because two-node keep-oldest downs the
// survivor on an oldest-node partition — see the class-doc deviation note.
var clusterA = Akka.Cluster.Cluster.Get(fixture.NodeA);
var victim = clusterA.SelfAddress;
clusterA.Leave(victim);
await TwoNodeClusterFixture.WaitForMemberRemoved(fixture.NodeB, victim, TimeSpan.FromSeconds(30));
// Release the port so the "restarted" original node can rebind it.
await fixture.NodeA.Terminate();
// Restart A on the same port — it rejoins as the YOUNGEST member but
// (lowest address) may regain Akka leadership.
var rejoined = fixture.StartNode(fixture.PortA, "Central");
try
{
await TwoNodeClusterFixture.WaitForMembersUp(rejoined, 2, TimeSpan.FromSeconds(30));
await TwoNodeClusterFixture.WaitForMembersUp(fixture.NodeB, 2, TimeSpan.FromSeconds(30));
var clusterB = Akka.Cluster.Cluster.Get(fixture.NodeB);
var clusterA2 = Akka.Cluster.Cluster.Get(rejoined);
// Product active-node semantics: survivor (oldest, singleton host)
// is active; the rejoined original node is standby — even when it
// holds Akka leadership.
Assert.True(ClusterActivityEvaluator.SelfIsOldest(clusterB),
"Survivor B must be the oldest Up member (the active/singleton-host node).");
Assert.False(ClusterActivityEvaluator.SelfIsOldest(clusterA2),
"Rejoined original node is the youngest member — it must NOT be active, " +
"even if it holds address-ordered Akka leadership (that is the divergence).");
}
finally
{
await rejoined.Terminate();
}
}
}