using System.Collections.Immutable;
using System.Reflection;
using Akka.Actor;
using Akka.Cluster;
using Akka.Configuration;
using Akka.Remote;
using Akka.TestKit.Xunit2;
using Akka.Util;
using ZB.MOM.WW.ScadaBridge.Host.Actors;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
///
/// ScadaBridge#132: the decision logic of . Cluster events and
/// are fed straight to the actor; the restart and
/// probe seams report to the test actor. Covers the single-side shut-out (restart, no tiebreak),
/// the tiebreak in both orders, the both-sides case where exactly one of two nodes restarts, the
/// winner's grace, and the probing that makes a healed partition produce the event at all. The
/// real two-node behaviour is proven in IntegrationTests (ShutOutRestartScenarioTests).
///
public class ShutOutRestartTests : TestKit
{
/// INFO so the decision-log assertions see every line.
public ShutOutRestartTests()
: base("akka.loglevel = INFO")
{
}
// "a" < "b" < "c" by Akka's member address ordering (host, then port).
private static readonly Address NodeA = new("akka.tcp", "scadabridge", "a", 8081);
private static readonly Address NodeB = new("akka.tcp", "scadabridge", "b", 8081);
private static readonly Address Stranger = new("akka.tcp", "scadabridge", "z", 9999);
private static readonly ShutOutRestartSettings Fast = new(
Enabled: true,
Grace: TimeSpan.FromSeconds(1.5),
ProbeInterval: TimeSpan.FromMilliseconds(200),
ProbeMaxInterval: TimeSpan.FromMilliseconds(800));
private sealed record Restarted(Address Self);
private sealed record Probed(Address Self, Address Peer);
/// Test double: restart and probe report to a probe actor.
private sealed class TestShutOutRestart : ShutOutRestartBase
{
private readonly IActorRef _probe;
private readonly Address[] _seeds;
public TestShutOutRestart(Address self, Address[] seeds, ShutOutRestartSettings settings, IActorRef probe)
: base(settings)
{
SelfAddress = self;
_seeds = seeds;
_probe = probe;
}
public override Address SelfAddress { get; }
public override IScheduler Scheduler => Context.System.Scheduler;
protected override IEnumerable
SeedNodes => _seeds;
protected override void RestartSelf() => _probe.Tell(new Restarted(SelfAddress));
protected override void Probe(Address peer) => _probe.Tell(new Probed(SelfAddress, peer));
}
private static Member CreateMember(Address address, MemberStatus status)
{
var create = typeof(Member).GetMethod(
"Create",
BindingFlags.Static | BindingFlags.NonPublic,
new[] { typeof(UniqueAddress), typeof(int), typeof(MemberStatus), typeof(ImmutableHashSet), typeof(AppVersion) })
?? throw new InvalidOperationException("Akka.Cluster.Member.Create(…) not found — Akka internals changed.");
return (Member)create.Invoke(null, new object[]
{
new UniqueAddress(address, 1), 1, status, ImmutableHashSet.Empty, AppVersion.Zero,
})!;
}
private static ClusterEvent.CurrentClusterState StateWith(params Member[] members) =>
new(
ImmutableSortedSet.CreateRange(members),
ImmutableHashSet.Empty,
ImmutableHashSet.Empty,
null,
ImmutableDictionary.Empty);
private IActorRef Node(Address self, Address peer, IActorRef? probe = null, ShutOutRestartSettings? settings = null) =>
Sys.ActorOf(Props.Create(() => new TestShutOutRestart(
self, new[] { self, peer }, settings ?? Fast, probe ?? TestActor)));
/// Both nodes see each other Up, then each removes the other after downing it (the partition outcome).
private static void BothRemovedEachOther(IActorRef node, Address self, Address peer)
{
node.Tell(StateWith(CreateMember(self, MemberStatus.Up), CreateMember(peer, MemberStatus.Up)));
node.Tell(new ClusterEvent.MemberRemoved(CreateMember(peer, MemberStatus.Removed), MemberStatus.Down));
}
private static ThisActorSystemQuarantinedEvent ShutOutBy(Address peer, Address self) => new(self, peer);
/// S2 as Akka.Remote 1.5.62 surfaces it: the endpoint writer's InvalidAssociation wrapping the protocol refusal.
private static AssociationErrorEvent StillRunningRemoved(Address peer, Address self) =>
new(
new Exception(
$"Association failed with {peer}",
new AggregateException(new Exception("The remote system has a UID that has been quarantined. Association aborted."))),
self, peer, inbound: false, Akka.Event.LogLevel.WarningLevel);
/// Everything receives during .
private static List