review(Cluster): record findings + fix snapshot consistency, dispose, stale docs

Code review at HEAD 7286d320. Cluster-001 (SeedFromCurrentState reads from one
snapshot), Cluster-003 (HoconLoader double-dispose), Cluster-004 (stale akka.conf
header), Cluster-005 (ServiceLevelCalculator tests added to Cluster.Tests). Cluster-002
deferred (no production caller).
This commit is contained in:
Joseph Doherty
2026-06-19 10:22:59 -04:00
parent 6dc74289ce
commit b1946194d6
5 changed files with 242 additions and 8 deletions
@@ -0,0 +1,91 @@
using Akka.Cluster;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Cluster.Redundancy;
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
/// <summary>
/// Unit tests for <see cref="ServiceLevelCalculator"/> — the pure ServiceLevel tiering logic.
/// Moved from ControlPlane.Tests to Cluster.Tests because the type lives in Core.Cluster.
/// See code-reviews/Cluster/findings.md Cluster-005.
/// </summary>
public sealed class ServiceLevelCalculatorTests
{
/// <summary>Non-Up member statuses produce ServiceLevel 0 regardless of other inputs.</summary>
[Theory]
[InlineData(MemberStatus.Down)]
[InlineData(MemberStatus.Removed)]
[InlineData(MemberStatus.Exiting)]
[InlineData(MemberStatus.Leaving)]
public void NotUp_returns_zero(MemberStatus status)
{
var sl = ServiceLevelCalculator.Compute(new(status,
DbReachable: true, OpcUaProbeOk: true, Stale: false, IsDriverRoleLeader: true));
sl.ShouldBe((byte)0);
}
/// <summary>Up + DB reachable + probe ok + not stale + not leader = tier 240 (healthy follower).</summary>
[Fact]
public void Fully_healthy_non_leader_returns_240()
{
var sl = ServiceLevelCalculator.Compute(new(MemberStatus.Up,
DbReachable: true, OpcUaProbeOk: true, Stale: false, IsDriverRoleLeader: false));
sl.ShouldBe((byte)240);
}
/// <summary>Up + DB reachable + probe ok + not stale + is leader = tier 250 (healthy leader, +10 bonus).</summary>
[Fact]
public void Fully_healthy_role_leader_returns_250()
{
var sl = ServiceLevelCalculator.Compute(new(MemberStatus.Up,
DbReachable: true, OpcUaProbeOk: true, Stale: false, IsDriverRoleLeader: true));
sl.ShouldBe((byte)250);
}
/// <summary>DB reachable but data stale = tier 200 (stale).</summary>
[Fact]
public void Db_reachable_but_stale_returns_200()
{
var sl = ServiceLevelCalculator.Compute(new(MemberStatus.Up,
DbReachable: true, OpcUaProbeOk: true, Stale: true, IsDriverRoleLeader: false));
sl.ShouldBe((byte)200);
}
/// <summary>DB unreachable AND stale = tier 100 (critically degraded).</summary>
[Fact]
public void Db_unreachable_and_stale_returns_100()
{
var sl = ServiceLevelCalculator.Compute(new(MemberStatus.Up,
DbReachable: false, OpcUaProbeOk: false, Stale: true, IsDriverRoleLeader: false));
sl.ShouldBe((byte)100);
}
/// <summary>OPC UA probe failed while not stale falls through to the catch-all 0.</summary>
[Fact]
public void Opcua_probe_fail_when_not_stale_returns_zero()
{
var sl = ServiceLevelCalculator.Compute(new(MemberStatus.Up,
DbReachable: true, OpcUaProbeOk: false, Stale: false, IsDriverRoleLeader: false));
sl.ShouldBe((byte)0);
}
/// <summary>Joining is treated identically to Up for grading (node is joining the cluster).</summary>
[Fact]
public void Joining_member_is_treated_like_Up()
{
var sl = ServiceLevelCalculator.Compute(new(MemberStatus.Joining,
DbReachable: true, OpcUaProbeOk: true, Stale: false, IsDriverRoleLeader: false));
sl.ShouldBe((byte)240);
}
/// <summary>Leader bonus is clamped to 255 even if a hypothetical basis would overflow.</summary>
[Fact]
public void Result_is_clamped_to_255()
{
// basis 240 + 10 = 250, already within byte range; confirms Clamp is in path
var sl = ServiceLevelCalculator.Compute(new(MemberStatus.Up,
DbReachable: true, OpcUaProbeOk: true, Stale: false, IsDriverRoleLeader: true));
((int)sl).ShouldBeLessThanOrEqualTo(255);
}
}