Handle OPC UA BadResourceUnavailable (0x80040000) from OtOpcUa's out-of-process script runner #112

Closed
opened 2026-11-09 23:17:00 -05:00 by dohertj2 · 2 comments
Owner

OtOpcUa is adding an out-of-process, pre-emptible script runner (dohertj2/lmxopcua#780, design docs/plans/2026-11-09-out-of-process-script-runner-design.md in lmxopcua, owner decision §14 #3). Once a node runs with Scripting:Runner=OutOfProcess, OtOpcUa publishes a new OPC UA status code on script-computed nodes:

Code Name When Reason text (OtOpcUa script log)
0x80040000 BadResourceUnavailable the script's worker process died under the evaluation (another script's hard-deadline kill, a crash, OOM) script worker restarted
0x80040000 BadResourceUnavailable no script worker became ready within Scripting:Worker:UnavailableGrace (default 5 s), or the worker cannot start at all (fail closed) script runner unavailable

What does not change:

  • Script-attributable faults (threw, timed out, refused, quarantined, compile error) stay BadInternalError (0x80020000).
  • The value carried with the Bad status is still the last-known value, published once per Good→Bad transition, and the node recovers to Good on the next successful evaluation.
  • Default is Scripting:Runner=InProcess, which never produces 0x80040000. Nothing changes on any deployed box until a node is explicitly flipped (design P3: QA first, Calculation then virtual tags; scripted alarms last and not yet covered).

Affected nodes: virtual tags (UNS namespace) and Calculation tags (raw namespace + every UNS reference to them). Scripted-alarm predicates are not affected yet (that seam stays in process in this phase; when it moves, a runner failure freezes the condition with a Bad quality annotation, it never clears the alarm).

Ask for ScadaBridge's OPC UA adapter (DCL): make sure 0x80040000 is handled as a plain Bad quality on a monitored item — "the source node is temporarily unable to compute this value", not a configuration, security or session error — so it maps to Bad quality on the instance attribute and does not trigger a reconnect, a subscription rebuild or a static-tag false-bad path. If the adapter classifies status codes by name or sub-code anywhere (quality mapping, alarm quality, flow-debugger display), add BadResourceUnavailable there. A test against a node that alternates Good / 0x80040000 / Good would pin it.

Must be in place before any OtOpcUa node that ScadaBridge reads is flipped to OutOfProcess (lmxopcua#780 P3).

Cross-link: dohertj2/lmxopcua#780.

OtOpcUa is adding an out-of-process, pre-emptible script runner (dohertj2/lmxopcua#780, design `docs/plans/2026-11-09-out-of-process-script-runner-design.md` in lmxopcua, owner decision §14 #3). Once a node runs with `Scripting:Runner=OutOfProcess`, OtOpcUa publishes a **new OPC UA status code on script-computed nodes**: | Code | Name | When | Reason text (OtOpcUa script log) | |---|---|---|---| | `0x80040000` | `BadResourceUnavailable` | the script's worker process died under the evaluation (another script's hard-deadline kill, a crash, OOM) | `script worker restarted` | | `0x80040000` | `BadResourceUnavailable` | no script worker became ready within `Scripting:Worker:UnavailableGrace` (default 5 s), or the worker cannot start at all (fail closed) | `script runner unavailable` | What does **not** change: - Script-attributable faults (threw, timed out, refused, quarantined, compile error) stay **`BadInternalError` (0x80020000)**. - The value carried with the Bad status is still the last-known value, published once per Good→Bad transition, and the node recovers to Good on the next successful evaluation. - Default is `Scripting:Runner=InProcess`, which never produces 0x80040000. Nothing changes on any deployed box until a node is explicitly flipped (design P3: QA first, Calculation then virtual tags; scripted alarms last and not yet covered). Affected nodes: virtual tags (UNS namespace) and Calculation tags (raw namespace + every UNS reference to them). Scripted-alarm predicates are not affected yet (that seam stays in process in this phase; when it moves, a runner failure freezes the condition with a Bad quality annotation, it never clears the alarm). **Ask for ScadaBridge's OPC UA adapter (DCL):** make sure 0x80040000 is handled as a plain Bad quality on a monitored item — "the source node is temporarily unable to compute this value", not a configuration, security or session error — so it maps to Bad quality on the instance attribute and does not trigger a reconnect, a subscription rebuild or a static-tag false-bad path. If the adapter classifies status codes by name or sub-code anywhere (quality mapping, alarm quality, flow-debugger display), add `BadResourceUnavailable` there. A test against a node that alternates Good / 0x80040000 / Good would pin it. Must be in place **before** any OtOpcUa node that ScadaBridge reads is flipped to `OutOfProcess` (lmxopcua#780 P3). Cross-link: dohertj2/lmxopcua#780.
dohertj2 added the type:followuppriority:p3cross-repo labels 2026-11-09 23:17:00 -05:00
Author
Owner

OtOpcUa side landed on lmxopcua master @ 69a6e0b4 (opt-in; default InProcess never emits 0x80040000). Tracking: dohertj2/lmxopcua#780.

OtOpcUa side landed on lmxopcua master @ 69a6e0b4 (opt-in; default InProcess never emits 0x80040000). Tracking: dohertj2/lmxopcua#780.
Author
Owner

Fixed on main @ 08fe5acf.

  • Adapter (OpcUaDataConnection): BadResourceUnavailable (code part 0x80040000, info bits ignored) maps to plain Bad tag quality as before; it never reaches RaiseDisconnected, and it is not in DataConnectionActor.IsConnectionLevelStatus, so no reconnect and no subscription rebuild. It is now logged on its own: one Information line per tag on entry ("source node is temporarily unable to compute this value…") and one on recovery, not one per notification. Read errors name the symbolic status (0x80040000 BadResourceUnavailable (source node temporarily unable to compute the value)). Every other Bad read now carries its symbolic name too.
  • Bad + null value (the drill note): InstanceActor used to overwrite the scalar attribute with null and re-notify scripts and alarms. Bad with no value is now a quality-only transition: the last-known value is kept, quality goes Bad, the update is published to the stream, and scripts, alarms and waiters are not re-evaluated. This matches the existing connection-drop rule (DCL design, "Connection drop detection"). A Bad update that does carry a value still stores it. This applies to every source, not only OtOpcUa, and also covers the DCL's own tag-resolution Bad+null push. Doc: Component-DataConnectionLayer.md.
  • Tests: OpcUaResourceUnavailableTests (Good, then 0x80040000, then 0x80040000, then Good: quality goes Good/Bad/Bad/Good, no Disconnected, no unsubscribe, exactly one entry log and one recovery log; the read names the status; 0x80040000 is not connection-level, with BadSessionIdInvalid as a positive control), and InstanceActor_BadQualityWithNullValue_KeepsLastKnownValue_AndRecoversOnGood plus a guard for Bad-with-value. Mutation-proved: with the InstanceActor branch disabled, and separately with the adapter tracking and naming reverted, 3 of the new tests fail.

Not covered: alarm quality and the flow-debugger display carry no OPC UA status code, only Good/Bad/Uncertain, so they need no change. Adjacent bug filed as #117 (non-zero Good codes map to Uncertain).

Evidence: dotnet build ZB.MOM.WW.ScadaBridge.slnx 0 warnings / 0 errors; DataConnectionLayer.Tests 483 passed (4 skipped), SiteRuntime.Tests 1016, Transport.Tests 232, Transport.IntegrationTests 151, ScriptAnalysis.Tests 103 — all green. Not deployed anywhere.

Fixed on main @ 08fe5acf. - **Adapter** (`OpcUaDataConnection`): `BadResourceUnavailable` (code part 0x80040000, info bits ignored) maps to plain Bad tag quality as before; it never reaches `RaiseDisconnected`, and it is not in `DataConnectionActor.IsConnectionLevelStatus`, so no reconnect and no subscription rebuild. It is now logged on its own: one Information line per tag on entry ("source node is temporarily unable to compute this value…") and one on recovery, not one per notification. Read errors name the symbolic status (`0x80040000 BadResourceUnavailable (source node temporarily unable to compute the value)`). Every other Bad read now carries its symbolic name too. - **Bad + null value** (the drill note): `InstanceActor` used to overwrite the scalar attribute with `null` and re-notify scripts and alarms. Bad with no value is now a quality-only transition: the last-known value is kept, quality goes Bad, the update is published to the stream, and scripts, alarms and waiters are not re-evaluated. This matches the existing connection-drop rule (DCL design, "Connection drop detection"). A Bad update that does carry a value still stores it. This applies to every source, not only OtOpcUa, and also covers the DCL's own tag-resolution Bad+null push. Doc: Component-DataConnectionLayer.md. - Tests: `OpcUaResourceUnavailableTests` (Good, then 0x80040000, then 0x80040000, then Good: quality goes Good/Bad/Bad/Good, no Disconnected, no unsubscribe, exactly one entry log and one recovery log; the read names the status; 0x80040000 is not connection-level, with BadSessionIdInvalid as a positive control), and `InstanceActor_BadQualityWithNullValue_KeepsLastKnownValue_AndRecoversOnGood` plus a guard for Bad-with-value. **Mutation-proved:** with the InstanceActor branch disabled, and separately with the adapter tracking and naming reverted, 3 of the new tests fail. Not covered: alarm quality and the flow-debugger display carry no OPC UA status code, only Good/Bad/Uncertain, so they need no change. Adjacent bug filed as #117 (non-zero Good codes map to Uncertain). Evidence: `dotnet build ZB.MOM.WW.ScadaBridge.slnx` 0 warnings / 0 errors; DataConnectionLayer.Tests 483 passed (4 skipped), SiteRuntime.Tests 1016, Transport.Tests 232, Transport.IntegrationTests 151, ScriptAnalysis.Tests 103 — all green. Not deployed anywhere.
Sign in to join this conversation.