fix(opcuaclient): scope the #670 F2 late-replay drop to the post-recovery window

The global 'never older than the last Good source time' rule froze a node
whenever an upstream clock stepped backwards (NTP correction, PLC clock
reset) until the clock caught up. The check is now a replay guard armed
only by a same-session recovery (keep-alive bad->good): per ref it holds
the last Good upstream source time (raised by the recovery read), a ref
disarms on its first in-order notification, and the whole guard expires
after max(30 s, 2x the longest subscription keep-alive window). Outside
it every notification is accepted. The steady-state tracker records the
LAST accepted time, not a high-water mark.
This commit is contained in:
Joseph Doherty
2026-10-11 09:15:51 -04:00
parent 99c13e4459
commit 87e8aff786
4 changed files with 157 additions and 27 deletions
+7 -5
View File
@@ -73,10 +73,12 @@ The session keep-alive (`KeepAliveInterval`, default 5 s) is the liveness signal
the refs still Bad are re-read once and published Good (a server whose subscription survived does not resend an
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.
A notification whose source time is OLDER than the newest Good upstream source time already published for its ref
(notification or recovery read) is dropped — the SDK can deliver a missed one seconds late after a same-session
recovery, which used to regress the node (live-gate F2). Equal timestamps pass (status-only changes); the outage
Bad never moves the comparison; a re-initialise resets it.
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
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
`Degraded` with its read stamp frozen, which is exactly the #527 comms-loss watchdog's trip condition. After 90 s
`DriverInstanceActor` runs `ShutdownAsync` + `InitializeAsync` (retried every 10 s while the server is down) and
@@ -234,7 +236,7 @@ Session/publish events are published on the shared OTel meter as the counter
`otopcua.driver.opcuaclient.events`, tagged `driver_instance` (the driver instance id) and `event` =
`publish_response`, `notification` (items), `data_change`, `bad_status_value`, `publish_error`,
`keepalive_miss`, `keepalive_recovered` (a bad keep-alive followed by a good one on the same session),
`stale_notification_dropped` (an out-of-order late notification, see above). A climbing
`stale_notification_dropped` (a late replay dropped inside the post-recovery window, see above). A climbing
`keepalive_miss` / `keepalive_recovered` rate is the signature of a flapping upstream (`reconnect_*` were removed with
the reconnect handler, #670). They are not part of `DriverHealth` or the
`/hosts` panel.