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:
Joseph Doherty
2026-10-11 11:35:50 -04:00
parent fccffd0a0f
commit 30a9d5f094
10 changed files with 491 additions and 111 deletions
@@ -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);