fix(opcuaclient): #676 review fix-ups (port-less rewrite, full reconcile, delivery lock, timers)
1. RewriteEndpointUrl keeps the advertised port when the configured URL names none (Uri.Port -1 would have sent the session to 4840); port-less + IPv6 tests. 2. The trailing re-read repairs any (value, status) difference that is not older than the last delivered value — Good->Bad/Uncertain included; a read without a source timestamp counts as newer; an equal source time with different content is published (repairs the replay guard's by-design equal-timestamp drop). 3. One driver-wide delivery lock makes decide+publish atomic across live notifications, the recovery read and the trailing read, so an older value can never land after a newer one (deterministic race test via a seam). 4. Known gap documented: a ConnectionLost raised while the host is not Connected is swallowed and that session falls back to the watchdog. 5. Only a SERVER-reported subscription loss raises ConnectionLost; the publishing-stall heuristic stays with the #527 watchdog. 6. Post-recovery timers run on Task.Delay(…, Clock, ct) with a session-work CTS cancelled on session release and dispose. 7. Replay summary at Information only when something was dropped (Debug otherwise); a re-arm reports the superseded window's drops and counts afresh. 8. DriverInstanceActor: AttachHostStatusSource doc comment restored; Reconnecting swallows a stray DisconnectObserved (test). 9. Stall wire test: 120 s session timeout, 3000-cycle subscription lifetime, one further bad keep-alive tick, 60 s headroom on the post-recovery waits (5/5 green).
This commit is contained in:
+23
-8
@@ -44,9 +44,12 @@ public sealed class OpcUaClientDriverStallWireTests
|
||||
await using var proxy = TcpStallProxy.Start(new Uri(rig.EndpointUrl).Port);
|
||||
OpcUaClientDriverWireTests.Publish(rig, OpcUaClientDriverWireTests.SpeedId, 10.0f);
|
||||
|
||||
// A 3 s operation timeout keeps the stall short: a keep-alive is declared failed when its request times out.
|
||||
// A 3 s operation timeout keeps the stall short (a keep-alive is declared failed when its request times out); a
|
||||
// 120 s session timeout and a 3000-cycle (300 s at 100 ms) subscription lifetime keep both far outside the
|
||||
// stall, so the test exercises the in-place recovery and nothing else.
|
||||
await using var drv = OpcUaClientDriverWireTests.NewDriver(
|
||||
rig, "wire-stall", endpointUrl: proxy.Through(rig.EndpointUrl), timeout: TimeSpan.FromSeconds(3));
|
||||
rig, "wire-stall", endpointUrl: proxy.Through(rig.EndpointUrl), timeout: TimeSpan.FromSeconds(3),
|
||||
sessionTimeout: TimeSpan.FromSeconds(120), subscriptionLifetimeCount: 3000);
|
||||
var connectionLost = 0;
|
||||
drv.ConnectionLost += (_, _) => Interlocked.Increment(ref connectionLost);
|
||||
await drv.InitializeAsync("{}", ct);
|
||||
@@ -63,10 +66,19 @@ public sealed class OpcUaClientDriverStallWireTests
|
||||
|
||||
var session = drv.Session.ShouldNotBeNull();
|
||||
var connectionsBefore = proxy.AcceptedConnections;
|
||||
// Bad keep-alive TICKS: the SDK reports a failed tick as two events at the same instant (the request's status and
|
||||
// BadNoCommunication), so events closer than half a keep-alive interval are one tick.
|
||||
var badKeepAlives = 0;
|
||||
var lastBadTick = DateTime.MinValue;
|
||||
session.KeepAlive += (_, e) =>
|
||||
{
|
||||
if (ServiceResult.IsBad(e.Status)) Interlocked.Increment(ref badKeepAlives);
|
||||
if (!ServiceResult.IsBad(e.Status)) return;
|
||||
lock (seen)
|
||||
{
|
||||
if (DateTime.UtcNow - lastBadTick < TimeSpan.FromMilliseconds(500)) return;
|
||||
lastBadTick = DateTime.UtcNow;
|
||||
badKeepAlives++;
|
||||
}
|
||||
};
|
||||
// The SDK's late backlog flush: a data-carrying NotificationMessage whose sequence number is BELOW one already
|
||||
// seen (keep-alive messages carry the next number). It is the positive control for the no-regression check.
|
||||
@@ -97,9 +109,10 @@ public sealed class OpcUaClientDriverStallWireTests
|
||||
await Task.Delay(300, ct); // > one publishing interval, so the server queues 20.0 as its own notification
|
||||
OpcUaClientDriverWireTests.Publish(rig, OpcUaClientDriverWireTests.SpeedId, 10.0f);
|
||||
|
||||
// Stays Bad and frozen across a further failed keep-alive (counted, not slept for).
|
||||
var badAtDetection = Volatile.Read(ref badKeepAlives);
|
||||
await WaitAsync(() => Volatile.Read(ref badKeepAlives) >= badAtDetection + 2, "further keep-alives must fail while stalled");
|
||||
// Stays Bad and frozen across one further failed keep-alive tick (counted, not slept for).
|
||||
int badAtDetection;
|
||||
lock (seen) badAtDetection = badKeepAlives;
|
||||
await WaitAsync(() => { lock (seen) return badKeepAlives > badAtDetection; }, "a further keep-alive must fail while stalled");
|
||||
drv.GetHealth().LastSuccessfulRead.ShouldBe(frozen, "the read stamp is the #527 watchdog's silence measure");
|
||||
lock (seen) seen.Count(s => s.Status == BadNoCommunication).ShouldBe(1, "one Bad per outage, not per keep-alive");
|
||||
|
||||
@@ -118,13 +131,15 @@ public sealed class OpcUaClientDriverStallWireTests
|
||||
// being answered or timed out — must still reach the node.
|
||||
OpcUaClientDriverWireTests.Publish(rig, OpcUaClientDriverWireTests.SpeedId, 40.0f);
|
||||
await WaitAsync(() => { lock (seen) return seen.Any(s => s is { Status: 0u, Value: 40.0f }); },
|
||||
"a change published right after recovery must reach the node (via the subscription or the trailing re-read)");
|
||||
"a change published right after recovery must reach the node (via the subscription or the trailing re-read)",
|
||||
TimeSpan.FromSeconds(60));
|
||||
await drv.ReconcileTaskForTest;
|
||||
|
||||
// The SDK flushes the notifications held during the stall seconds after recovery (the #670 F2 shape). Wait for
|
||||
// that flush before judging ordering, or the check below would pass on a run where nothing late arrived.
|
||||
await WaitAsync(() => { lock (seen) return lateMessages > 0; },
|
||||
"the SDK's late backlog flush did not arrive — the no-regression check below would be vacuous");
|
||||
"the SDK's late backlog flush did not arrive — the no-regression check below would be vacuous",
|
||||
TimeSpan.FromSeconds(60));
|
||||
|
||||
lock (seen)
|
||||
{
|
||||
|
||||
+4
-2
@@ -41,7 +41,8 @@ public sealed class OpcUaClientDriverWireTests
|
||||
|
||||
internal static OpcUaClientDriver NewDriver(
|
||||
ServerEventRig rig, string id, string? browseRoot = null, IReadOnlyList<RawTagEntry>? extraTags = null,
|
||||
string? endpointUrl = null, TimeSpan? timeout = null) =>
|
||||
string? endpointUrl = null, TimeSpan? timeout = null, TimeSpan? sessionTimeout = null,
|
||||
uint? subscriptionLifetimeCount = null) =>
|
||||
new(new OpcUaClientDriverOptions
|
||||
{
|
||||
EndpointUrl = endpointUrl ?? rig.EndpointUrl,
|
||||
@@ -59,7 +60,8 @@ public sealed class OpcUaClientDriverWireTests
|
||||
Password = ServerEventRig.KeyPassword,
|
||||
AutoAcceptCertificates = true,
|
||||
Timeout = timeout ?? TimeSpan.FromSeconds(10),
|
||||
SessionTimeout = TimeSpan.FromSeconds(30),
|
||||
SessionTimeout = sessionTimeout ?? TimeSpan.FromSeconds(30),
|
||||
SubscriptionLifetimeCount = subscriptionLifetimeCount ?? 1000,
|
||||
KeepAliveInterval = TimeSpan.FromSeconds(1),
|
||||
BrowseRoot = browseRoot,
|
||||
}, id);
|
||||
|
||||
+18
@@ -1,4 +1,5 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Event;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
@@ -126,6 +127,23 @@ public sealed class DriverInstanceActorConnectionLossSignalTests : RuntimeActorT
|
||||
driver.InitializeCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>Review of 3b88056f: a watchdog DisconnectObserved still queued when the connection-loss signal has
|
||||
/// already moved the actor to Reconnecting is a no-op there, not an Unhandled message.</summary>
|
||||
[Fact]
|
||||
public async Task A_stray_DisconnectObserved_while_reconnecting_is_not_unhandled()
|
||||
{
|
||||
var driver = new LossSourceStubDriver();
|
||||
var (actor, parent) = Connect(driver);
|
||||
var unhandled = CreateTestProbe();
|
||||
Sys.EventStream.Subscribe(unhandled.Ref, typeof(UnhandledMessage));
|
||||
await driver.RaiseAsync("keep-alive reported BadServerHalted");
|
||||
parent.ExpectMsg<DriverInstanceActor.ConnectivityChanged>(TimeSpan.FromSeconds(3)).Connected.ShouldBeFalse();
|
||||
|
||||
actor.Tell(new DriverInstanceActor.DisconnectObserved("late watchdog trip"));
|
||||
|
||||
unhandled.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_handler_is_attached_once_and_detached_when_the_actor_stops()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user