fix(opcuaclient): #670 F2 re-gate — hold the replay guard for the whole window

The F2 live gate failed (10 value regressions over 4 blips): IsLateReplay
disarmed a ref on its first in-order notification, which arrives ~1 s after
recovery, while the SDK's late replay lands 4-6 s later. Equal source times
also passed, so a replay equal to the recovery read got through.

While the window is open each ref now carries a high-water mark (seeded at
arming, raised by the recovery read, advanced by every accepted notification);
a notification at or below it is dropped (equal included — OPC UA moves the
source timestamp on every value/status change). Null source time passes
without moving the mark. Window expiry still disarms entirely, so an upstream
clock step-back is never frozen outside the window. Arm/expiry serialised by a
lock so an expiry racing a re-arm cannot wipe fresh seeds.
This commit is contained in:
Joseph Doherty
2026-10-11 09:42:39 -04:00
parent 57deaeba16
commit 099bc1b6e0
5 changed files with 146 additions and 33 deletions
+7 -3
View File
@@ -74,9 +74,13 @@ The session keep-alive (`KeepAliveInterval`, default 5 s) is the liveness signal
unchanged value, and a `(value, status)` dedup downstream lets the identical value through), alarm quality goes
back Good and a `ConditionRefresh` re-reports anything missed. A notification that lands first wins over the read.
For a bounded **replay window** after such a recovery — max(30 s, 2 x the longest subscription keep-alive window) —
a notification whose source time is OLDER than the ref's last Good upstream source time (raised by the recovery
read) is dropped: the SDK was seen delivering a missed one 4–5 s late, which regressed the node (live-gate F2).
A ref disarms on its first in-order (equal or newer) notification. Outside that window every notification is
each ref is guarded by a high-water mark — its last Good upstream source time, raised by the recovery read and
advanced by every notification accepted in the window — and a notification whose source time is at or BELOW it
(equal included: OPC UA moves the source timestamp on every value or status change, so an equal one is a replay)
is dropped: the SDK was seen delivering a missed one 4–6 s late, which regressed the node (live-gate F2). The
guard holds for the WHOLE window — fresh notifications arrive ~1 s after recovery, before the late replay, so a
ref is never disarmed early (the F2 re-gate failed on exactly that). A notification with no source time passes
without moving the mark. Outside that window every notification is
accepted whatever its timestamp, so an upstream clock that steps backwards (NTP, a PLC clock reset) never
freezes a node. The outage Bad never moves the comparison; a re-initialise resets it.
- **The session does not come back** (an upstream restart: the server forgot the session) ⇒ the driver stays