fix(opcuaclient): #676 live gate A1 — a sustained closed connection is a definitive loss
Live gate A1 (docker-dev, opc-plc restart / SIGINT / kill): the fast path never fired — a real process stop delivers no session status; the keep-alives report BadSecureChannelClosed (0x80860000) once, then BadConnectionClosed (0x80AE0000) interleaved with BadNoCommunication for the whole outage. Reproduced in-process with TcpStallProxy.HardClose (reset every connection, refuse new ones; server and session alive behind it): the identical sequence, and the SDK 1.5.378 session never opens a new connection on its own (48 s with the proxy accepting again, zero attempts) — a closed channel cannot recover in place. The driver now treats BadSecureChannelClosed/BadConnectionClosed as definitive on the SECOND closed-channel keep-alive failure in one outage (a good keep-alive resets the count), raising ConnectionLost ~1 keep-alive after the drop. BadNoCommunication / BadRequestTimeout / BadNotConnected (stall/pause) stay with the watchdog; the stall test still sees no ConnectionLost. New wire test (hard close + reopen ⇒ raised once, SDK never reconnects, reinit brings the identical value back Good) and unit tests (sustained raises once; a single blip that recovers does not) — all red on the previous code. Docs updated.
This commit is contained in:
@@ -458,11 +458,15 @@ alternatives:
|
||||
communication" — must not raise: a stalled link that resumes keeps its session, subscriptions and in-place recovery,
|
||||
and tearing it down would cost all three. Those stay with the watchdog, whose conjunction is the honest test for them.
|
||||
- **Today only `OpcUaClientDriver` implements it:** keep-alive `BadSessionIdInvalid` / `BadSessionClosed` /
|
||||
`BadSessionNotActivated` / `BadServerHalted`, or a subscription the server REPORTED dropped (not the driver's
|
||||
`BadSessionNotActivated` / `BadServerHalted`; a CLOSED connection (`BadSecureChannelClosed` /
|
||||
`BadConnectionClosed`) once sustained — the second closed-channel keep-alive in one outage, which is what a real
|
||||
process stop actually produces (live gate A1), and the SDK never reopens a closed connection by itself; or a
|
||||
subscription the server REPORTED dropped (not the driver's
|
||||
publishing-stall heuristic, which is an inference from silence and stays with the watchdog; see
|
||||
[OpcUaClient.md](drivers/OpcUaClient.md) §"Upstream outage and recovery" for why `BadServerHalted` is on the list —
|
||||
it was measured, the SDK never re-opens a halted channel). A gracefully restarted upstream therefore recovers in
|
||||
about one reconnect interval instead of ~100 s; a killed one still waits for the watchdog.
|
||||
it was measured, the SDK never re-opens a halted channel). A restarted, stopped or killed upstream therefore
|
||||
recovers in about one keep-alive plus one reconnect interval instead of ~100 s; a stall/pause
|
||||
(`BadRequestTimeout`/`BadNoCommunication`) still goes through the watchdog and keeps its in-place recovery.
|
||||
- ⚠️ **Known gap:** "only while `Connected`" plus "once per connection" means a raise the host receives while not yet
|
||||
Connected (e.g. the protocol proves the fresh connection dead before `InitializeSucceeded` is processed) is swallowed,
|
||||
and that connection falls back to the watchdog — the pre-#676 recovery time, in a narrow window.
|
||||
|
||||
@@ -137,11 +137,21 @@ The session keep-alive (`KeepAliveInterval`, default 5 s) is the liveness signal
|
||||
because it was **measured**: on the restart wire test a gracefully stopped server reports `BadServerHalted` (and the
|
||||
keep-alives alternate it with `BadNoCommunication`), and the SDK never re-opens a channel the server halted — 15 s
|
||||
after the server was back, the same session still failed every request with `BadServerHalted`; `BadSessionIdInvalid`
|
||||
never arrived. Everything else — `BadNoCommunication`, `BadConnectionClosed`, `BadNotConnected`,
|
||||
`BadRequestTimeout`/`BadTimeout` (what a stalled link produces) — is **ambiguous** (the link may resume and keep its
|
||||
session, see the stall test) and stays with the watchdog. Consequence: a gracefully restarted upstream recovers in
|
||||
roughly one reconnect interval (≈10 s) instead of ~100 s; an upstream that vanishes without a halt (killed, cable
|
||||
pulled) still waits for the watchdog. ⚠️ **Known gap:** the host acts on the signal only while the driver is
|
||||
never arrived.
|
||||
**A CLOSED connection is definitive too, once sustained (live gate A1).** Against a real opc-plc on docker-dev, none of
|
||||
`docker restart`, a graceful SIGINT stop (opc-plc itself logged `BadServerHalted`) or `docker kill` delivered a
|
||||
session status to the client: the keep-alives reported `BadSecureChannelClosed` (0x80860000) once, then
|
||||
`BadConnectionClosed` (0x80AE0000) interleaved with `BadNoCommunication` for the whole outage, so the first cut of the
|
||||
fast path never fired and recovery took 91–127 s. Reproduced in-process with `TcpStallProxy.HardClose` (every
|
||||
connection reset, new ones refused, the server AND its session still alive behind it): the same sequence, and the SDK
|
||||
1.5.378 session **never opens a new connection by itself** — 48 s after the proxy accepted connections again, not one
|
||||
attempt. So a closed channel can never recover in place; on the **second** closed-channel keep-alive failure in one
|
||||
outage (`IsClosedChannel`; a good keep-alive resets the count, so a single transient does not count) the driver raises
|
||||
`ConnectionLost` — about one keep-alive interval after the connection dropped. Everything else —
|
||||
`BadNoCommunication`, `BadNotConnected`, `BadRequestTimeout`/`BadTimeout` (what a stalled or paused link produces) —
|
||||
is **ambiguous** (the link may resume and keep its session, see the stall test) and stays with the watchdog.
|
||||
Consequence: a restarted, stopped or killed upstream is re-initialised within roughly one keep-alive plus one
|
||||
reconnect interval instead of ~100 s; a pause (`docker pause`, a congested link) still recovers in place. ⚠️ **Known gap:** the host acts on the signal only while the driver is
|
||||
`Connected`, and the driver raises it once per session — so a definitive loss raised while the host is not
|
||||
Connected (e.g. `BadServerHalted` on the fresh session before the host has processed the successful initialise) is
|
||||
swallowed and that session falls back to the watchdog. Re-raising on later keep-alives would close it but was left out
|
||||
@@ -318,7 +328,10 @@ the reconnect handler, #670). They are not part of `DriverHealth` or the
|
||||
used to be live-gate-only: the driver connects through `TcpStallProxy`, an in-test TCP forwarder whose pause holds
|
||||
every byte without closing anything (which only works because of the endpoint rewrite above); it asserts one Bad,
|
||||
a frozen read stamp, recovery on the SAME session, the identical value republished Good, a change published right
|
||||
after recovery still arriving, and no source-time regression once the SDK's late backlog flush has arrived.
|
||||
after recovery still arriving, and no source-time regression once the SDK's late backlog flush has arrived. Its
|
||||
second test uses `HardClose` (reset every connection, refuse new ones) — the shape of a real process stop — and
|
||||
asserts the live-gate status sequence, that the SDK never reconnects on its own once the proxy accepts again,
|
||||
`ConnectionLost` exactly once, and the re-initialise bringing the identical value back Good.
|
||||
`OpcUaClientDriverReconnectWireTests` covers the server restart (incl. the `BadServerHalted` → `ConnectionLost` signal)
|
||||
and the subscription-lost case.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user