4 Commits

Author SHA1 Message Date
Joseph Doherty 1d7a4b7a53 test: pass TestContext.Current.CancellationToken at xUnit1051 call sites (analyzer code fix, 451 sites) (lmxopcua#725)
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 13:19:34 -05:00
Joseph Doherty e3cb4f0c7b fix(ablegacy): opaque replies are neither answers nor faults; round-2 minors
Review r1 N1 (Important): the I1 fix made IsDeviceRefusal default-open and
mapped libplctag's ERR_REMOTE_ERR to an answer. libplctag folds EVERY non-zero
PCCC STS and CIP status into that code, including a routing hop's "unconnected
send timed out" / "connection lost" / "remote node missing", and native
BadReply covers truncated / mismatched / unmapped replies - so a powered-off
PLC behind a bridge read as reachable: Healthy, LastSuccessfulRead advancing,
the comms-loss watchdog unable to fire.

Three classes now, each pinned both ways:
- no reply (Timeout/BadConnection/Abort/Pending/unknown): comms fault;
- answered - an ALLOW-LIST again (NotFound/NotAllowed/OutOfBounds/TooLarge/
  TooSmall/TypeMismatch/DeviceStateConflict): proves reachability;
- OPAQUE (BadReply/Partial; ERR_REMOTE_ERR/ERR_BAD_STATUS now translate to the
  existing BadReply, OPC UA BadCommunicationError as before r0): per-tag Bad,
  handle kept, coalescing fallback, never fail-fast, never an answer. Option
  (a): a device whose pass/call produced only opaque outcomes is Degraded with
  LastSuccessfulRead not advanced, so the watchdog reaches a dead routed PLC.
  The probe and index reads classify the same way.

Minors:
- N2: a mismatched array (type vs data file) READS BadTypeMismatch instead of
  decoding the file at the wrong width as Good.
- N3: a swept indirect resolution is never reconnected under a forgotten gate;
  that one call answers neutrally.
- N4: an index read answered Unsupported is neutral, not a transport fault.
- N5: ArrayFileMismatch checks indirect addresses (the file letter is static).
- Test nit: the evicted-while-waiting test signals gate arrival via a seam
  instead of sleeping.

Live: 51/51 on ab_server (slc500/micrologix/plc5 x both transports), incl. an
opaque-only libplctag device staying Degraded with LastSuccessfulRead frozen.

Refs #738
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:45:04 -04:00
Joseph Doherty 16c2045cab fix(ablegacy): never operate on a handle evicted while waiting for its gate
Review r0 M3. The indirect sweep disposed stale resolutions without holding
their gates, and any reader/writer that fetched a handle before waiting on its
gate could then use an instance a sibling had evicted and disposed - a
spurious comms fault and Degraded. The sweep now evicts under each key's gate,
and read/write re-resolve the handle once the gate is held (reconnecting
single-flight if it was evicted; a retired state never reconnects).

Refs #738
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:22:17 -04:00
Joseph Doherty 17ae2eea0b fix(ablegacy): any answer restores Healthy; writes feed health and the backoff
Review r0 I2 + M1/M2/M4/M7/M10.
- I2: one health writer (ApplyHealth) for reads AND writes, worst wins per
  call: a comms fault => Degraded; otherwise any answer (Good value, device
  refusal, Good or refused write) => Healthy; LastSuccessfulRead advances on
  any answer. A refusal-only read used to leave a recovered driver Degraded,
  and writes never touched health at all.
- M10: a no-reply write STATUS now degrades like the exception path did; a
  Good/refused write records an answer on the device's backoff, a no-reply
  one a fault (bit RMW included — it now reports its raw status).
- M2: a write inside the device's backoff window fails fast with
  BadCommunicationError even when its handle is cached.
- M1: ShutdownAsync/Dispose write their final Unknown only while the epoch
  they retired is still current (a newer initialise owns health otherwise).
- M4: an indirect bit write releases the previous index value's parent-word
  handle (the per-word RMW gate is deliberately kept: removing a gate another
  writer may hold would let two RMWs of one word race).
- M7: backoff decisions read an injectable Clock; the isolation tests drop
  their stopwatch asserts and cross the window on a manual clock.

Refs #738
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 15:16:49 -04:00