fix(opcuaclient): unresolvable acks warn instead of tripping the ack breaker; shutdown holds subscription management (review r1 N-1, N-2, N-3)

- N-1 (Important): m3's BadNodeIdUnknown throw for an ack with no resolvable
  condition counted toward the host's AlarmAcknowledge breaker (threshold 5), so
  re-acks of one unresolvable condition blocked every genuine ack. It now logs a
  Warning and returns; only a no-reply transport failure throws. Test through the
  real CapabilityInvoker: six unresolvable acks, then a genuine ack that is still
  delivered (red with the throw restored).
- N-2: ShutdownAsync holds _subscriptionGate for the whole teardown (in-flight
  subscribes finish first, bounded by the budget), so a subscribe arriving during
  the delete phase finds the session released instead of creating a subscription
  on the closing session. Red-tested.
- N-3: the shutdown-side cert-validation detach is generation-checked under
  _probeLock with the map / endpoint clear.

Refs #743

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 16:58:37 -04:00
parent 863a00c25d
commit c39bcd6e24
4 changed files with 155 additions and 23 deletions
+4 -2
View File
@@ -104,8 +104,10 @@ The session keep-alive (`KeepAliveInterval`, default 5 s) is the liveness signal
no health — including a write the host cancelled at its own write deadline against a hung upstream; the session
keep-alive owns liveness and reports that outage itself. A reply that was in flight across a bad keep-alive advances
the stamp but does not write `Healthy` while the outage is published — the keep-alive recovery ends the outage.
An ack none of whose condition ids resolves sends nothing and fails `BadNodeIdUnknown`; with no session an ack fails
`BadNotConnected` (before or inside the gate alike).
An ack none of whose condition ids resolves sends nothing and logs a Warning — it does **not** throw (a deterministic
per-ack refusal must not count toward the host's AlarmAcknowledge circuit breaker, or five re-acks of one
unresolvable condition would block every genuine ack). Only a call with no reply throws; with no session an ack
fails `BadNotConnected` (before or inside the gate alike).
- **Bad keep-alive** ⇒ health `Degraded` (`LastError = "keep-alive: <status>"`, read stamp preserved), host status
`Stopped`, and — **once per outage**, not per keep-alive — every subscribed ref is published `BadNoCommunication`
with its last value retained and **source + server timestamp = the detection time** (live-gate F1: stamped with the