test(redundancy): cover stale-probe-not-demoted branch + make _probeFreshnessWindow readonly (code-review)

This commit is contained in:
Joseph Doherty
2026-06-15 13:11:01 -04:00
parent cf278035d2
commit 5382eea9b5
2 changed files with 45 additions and 1 deletions
@@ -59,7 +59,7 @@ public sealed class OpcUaPublishActor : ReceiveActor
private readonly Phase7Applier? _applier;
private readonly IActorRef? _dbHealthProbe;
private readonly TimeSpan _staleWindow;
private TimeSpan _probeFreshnessWindow;
private readonly TimeSpan _probeFreshnessWindow;
private readonly Akka.Cluster.Cluster _cluster = Akka.Cluster.Cluster.Get(Context.System);
private readonly ILoggingAdapter _log = Context.GetLogger();
@@ -355,6 +355,50 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Verifies branch (3) of <c>OpcUaProbeOk()</c>: a peer's NEGATIVE verdict about this node
/// that has aged past <c>_probeFreshnessWindow</c> is given the benefit of the doubt (the peer's
/// verdict aged out → don't demote), so the node still publishes the HEALTHY level. With a 1ms
/// freshness window and a 30ms wait before the recompute, the cached <c>Ok==false</c> verdict is
/// reliably stale by the time the level is computed, so <c>OpcUaProbeOk()</c> returns true and a
/// healthy primary-leader computes inputs (true, true, false) → 240, +10 leader → 250 — NOT the 0
/// that a still-fresh negative verdict would produce (cf. <see cref="Probe_false_about_me_with_healthy_db_publishes_0"/>).</summary>
[Fact]
public void Stale_probe_verdict_is_not_demoted_publishes_healthy()
{
var publisher = new RecordingPublisher();
var local = NodeId.Parse("primary-node");
var probe = Sys.ActorOf(Akka.Actor.Props.Create(() =>
new StubDbHealth(new DbHealthProbeActor.DbHealthStatus(true, DateTime.UtcNow, null))));
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
serviceLevel: publisher, localNode: local, dbHealthProbe: probe,
staleWindow: TimeSpan.FromSeconds(30),
probeFreshnessWindow: TimeSpan.FromMilliseconds(1)));
// Seed a healthy DB sample and a NEGATIVE peer verdict about me. (No RedundancyStateChanged yet,
// so no level is computed from these — the verdict just gets stamped with the receive time.)
actor.Tell(new DbHealthProbeActor.DbHealthStatus(true, DateTime.UtcNow, null));
actor.Tell(new PeerOpcUaProbeActor.OpcUaProbeResult(local, Ok: false));
// Let the verdict age WELL past the 1ms freshness window before any level is computed. The 30ms
// wait is >> 1ms, so the verdict is reliably stale by the time OpcUaProbeOk() runs — no reliance
// on sub-ms clock ties.
ExpectNoMsg(TimeSpan.FromMilliseconds(30));
// Now trigger the recompute with a fresh healthy primary-leader snapshot. By now _probeAboutMe is
// >1ms old, so branch (3) ignores the negative verdict and OpcUaProbeOk() returns true.
actor.Tell(new RedundancyStateChanged(
Nodes: new[]
{
new NodeRedundancyState(local, RedundancyRole.Primary,
IsClusterLeader: true, IsRoleLeaderForDriver: true, DateTime.UtcNow),
},
CorrelationId.NewId()));
// Healthy (250), NOT 0 — proves an aged negative verdict does not demote.
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
duration: TimeSpan.FromMilliseconds(500));
}
/// <summary>Verifies that with no peer probe result ever received, <c>OpcUaProbeOk()</c> defaults
/// to <c>true</c> (benefit of the doubt / single-node). A healthy primary-leader thus computes
/// inputs (true, true, false) → 240, +10 leader → 250.</summary>