fix(redundancy): a failed OPC UA endpoint drops ServiceLevel to 0 and steps the Primary down (lmxopcua#762)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
IOpcUaEndpointState (review-remediation lane C3) recorded whether the SDK started, but only the health check read it: a fused node whose endpoint failed (port 4840 taken, ...) still advertised 250/240 and stayed driver Primary, so the Primary-gated data plane ran on a node no client could reach while the reachable peer refused writes as Secondary. - Move IOpcUaEndpointState/OpcUaEndpointState/OpcUaEndpointStatus from Host to Commons.OpcUa so the Runtime can consume it. - ServiceLevelCalculator: new NodeHealthInputs.EndpointListening (default true). false -> 0, ahead of every other input including the +10 Primary bonus. - OpcUaPublishActor: takes the endpoint state, feeds EndpointListening (Failed only; NotStarted / Stopped are boot/shutdown windows) on the DB-backed and DB-less paths, and the legacy role-only seam returns 0 for a failed endpoint too. - New OpcUaEndpointStepDownActor (one per driver node): a Primary (ClusterActiveNode.SelfIsActive, the election's own primitive) with a Failed endpoint gracefully Leaves its mesh - the manual failover mechanism - so the peer becomes the oldest Up driver member, hosts the singletons and is Primary. The election itself is not bent. Guarded on a peer having recently reported its own endpoint listening (EndpointReport on the new opcua-endpoint-state DPS topic), so a pair with both endpoints failed does not bounce the role and restart nodes forever, and a lone node never shuts itself down. - Runtime now references ZB.MOM.WW.Health.Akka (ClusterActiveNode). - Docs: Redundancy.md tier table + new "Endpoint start failure: Primary step-down" section; cluster-topology rule; HealthEndpoints / OpcUaEndpointHealthCheck comments (F-C1 landed). Tests: ServiceLevelCalculatorTests (+5), OpcUaPublishActorEndpointStateTests (7: DB-less, DB-backed, legacy seam, NotStarted/Stopped do not demote, a later failure demotes on the next tick), OpcUaEndpointStepDownActorTests (3, real two-node mesh: holds while the peer is not listening then leaves and the peer becomes Primary; failed Secondary and healthy Primary never leave). Each was mutation-checked: removing the calculator/legacy guard, the Leave, the peer-listening guard or the Primary guard fails the corresponding tests.
This commit is contained in:
@@ -88,4 +88,32 @@ public sealed class ServiceLevelCalculatorTests
|
||||
DbReachable: true, OpcUaProbeOk: true, Stale: false, IsDriverPrimary: true));
|
||||
((int)sl).ShouldBeLessThanOrEqualTo(255);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// lmxopcua#762: a node whose own OPC UA endpoint failed to start computes 0 — whatever every other input
|
||||
/// says, and in particular without the +10 Primary bonus that used to keep a dead endpoint at 250.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(true, true, false, true)] // would be 250
|
||||
[InlineData(true, true, false, false)] // would be 240
|
||||
[InlineData(true, true, true, true)] // would be 210
|
||||
[InlineData(false, true, true, false)] // would be 100
|
||||
public void Endpoint_not_listening_returns_zero(bool dbReachable, bool probeOk, bool stale, bool primary)
|
||||
{
|
||||
var healthy = ServiceLevelCalculator.Compute(new(MemberStatus.Up,
|
||||
DbReachable: dbReachable, OpcUaProbeOk: probeOk, Stale: stale, IsDriverPrimary: primary));
|
||||
healthy.ShouldBeGreaterThan((byte)0, "positive control: the same inputs with a listening endpoint must not already be 0");
|
||||
|
||||
var sl = ServiceLevelCalculator.Compute(new(MemberStatus.Up,
|
||||
DbReachable: dbReachable, OpcUaProbeOk: probeOk, Stale: stale, IsDriverPrimary: primary,
|
||||
EndpointListening: false));
|
||||
sl.ShouldBe((byte)0);
|
||||
}
|
||||
|
||||
/// <summary>The endpoint input defaults to listening, so callers with no endpoint fact keep the old tiering.</summary>
|
||||
[Fact]
|
||||
public void Endpoint_listening_defaults_to_true()
|
||||
{
|
||||
new NodeHealthInputs(MemberStatus.Up, true, true, false, true).EndpointListening.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user