feat(redundancy): OpcUaProbeOk from peer-probes-me with freshness debounce
This commit is contained in:
@@ -326,6 +326,119 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an actively-observed, recent peer probe of MY endpoint that came back
|
||||
/// <c>Ok==false</c> demotes the calculator basis to 0. A non-leader Secondary entry (no +10 bonus)
|
||||
/// is used so the assertion is unambiguous: inputs (DbReachable=true, OpcUaProbeOk=false,
|
||||
/// Stale=false) match Compute's <c>_ => 0</c> arm → 0, +0 (non-leader) → 0.</summary>
|
||||
[Fact]
|
||||
public void Probe_false_about_me_with_healthy_db_publishes_0()
|
||||
{
|
||||
var publisher = new RecordingPublisher();
|
||||
var local = NodeId.Parse("secondary-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.FromSeconds(30)));
|
||||
|
||||
actor.Tell(new DbHealthProbeActor.DbHealthStatus(true, DateTime.UtcNow, null));
|
||||
actor.Tell(new PeerOpcUaProbeActor.OpcUaProbeResult(local, Ok: false));
|
||||
actor.Tell(new RedundancyStateChanged(
|
||||
Nodes: new[]
|
||||
{
|
||||
new NodeRedundancyState(local, RedundancyRole.Secondary,
|
||||
IsClusterLeader: false, IsRoleLeaderForDriver: false, DateTime.UtcNow),
|
||||
},
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)0),
|
||||
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>
|
||||
[Fact]
|
||||
public void No_probe_result_defaults_ok_true_publishes_250()
|
||||
{
|
||||
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.FromSeconds(30)));
|
||||
|
||||
actor.Tell(new DbHealthProbeActor.DbHealthStatus(true, DateTime.UtcNow, null));
|
||||
actor.Tell(new RedundancyStateChanged(
|
||||
Nodes: new[]
|
||||
{
|
||||
new NodeRedundancyState(local, RedundancyRole.Primary,
|
||||
IsClusterLeader: true, IsRoleLeaderForDriver: true, DateTime.UtcNow),
|
||||
},
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a later <c>Ok==true</c> peer probe supersedes an earlier <c>Ok==false</c>
|
||||
/// (recovery). A non-leader Secondary entry is used (no +10 bonus): after the true supersedes,
|
||||
/// inputs (true, true, false) → 240, +0 → 240.</summary>
|
||||
[Fact]
|
||||
public void Probe_true_supersedes_earlier_false_publishes_240()
|
||||
{
|
||||
var publisher = new RecordingPublisher();
|
||||
var local = NodeId.Parse("secondary-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.FromSeconds(30)));
|
||||
|
||||
actor.Tell(new DbHealthProbeActor.DbHealthStatus(true, DateTime.UtcNow, null));
|
||||
actor.Tell(new PeerOpcUaProbeActor.OpcUaProbeResult(local, Ok: false));
|
||||
actor.Tell(new PeerOpcUaProbeActor.OpcUaProbeResult(local, Ok: true));
|
||||
actor.Tell(new RedundancyStateChanged(
|
||||
Nodes: new[]
|
||||
{
|
||||
new NodeRedundancyState(local, RedundancyRole.Secondary,
|
||||
IsClusterLeader: false, IsRoleLeaderForDriver: false, DateTime.UtcNow),
|
||||
},
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)240),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a peer probe result about a DIFFERENT node is ignored — it does not
|
||||
/// affect MY <c>OpcUaProbeOk()</c>, which stays at its default <c>true</c>. A healthy
|
||||
/// primary-leader thus still computes (true, true, false) → 240, +10 → 250.</summary>
|
||||
[Fact]
|
||||
public void Probe_about_a_different_node_is_ignored()
|
||||
{
|
||||
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.FromSeconds(30)));
|
||||
|
||||
actor.Tell(new DbHealthProbeActor.DbHealthStatus(true, DateTime.UtcNow, null));
|
||||
actor.Tell(new PeerOpcUaProbeActor.OpcUaProbeResult(NodeId.Parse("someone-else"), Ok: false));
|
||||
actor.Tell(new RedundancyStateChanged(
|
||||
Nodes: new[]
|
||||
{
|
||||
new NodeRedundancyState(local, RedundancyRole.Primary,
|
||||
IsClusterLeader: true, IsRoleLeaderForDriver: true, DateTime.UtcNow),
|
||||
},
|
||||
CorrelationId.NewId()));
|
||||
|
||||
AwaitAssert(() => publisher.Levels.ShouldContain((byte)250),
|
||||
duration: TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the legacy back-compat seam: with no DB-health probe wired, the handler
|
||||
/// falls back to the old role-only switch (Primary + leader → 240).</summary>
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user