OpcUaClient respawn replays inactive+unacked conditions as Raise+Clear (duplicate rows, Active flicker) #657

Closed
opened 2026-10-09 00:15:09 -04:00 by dohertj2 · 2 comments
Owner

Found in the #656 review (batch hw3). Pre-existing, outside #656's scope.

  1. After an OpcUaClient driver respawn, every inactive+unacked condition the upstream replays goes through Raise→Clear (OpcUaClientDriver.cs ~2114-2116). That writes a duplicate "Activated" + "Cleared" /alerts pair, and clients briefly see the condition Active again.
  2. An OpcUaClient first-sight inactive+acked event (OpcUaClientDriver.cs ~1946-1955) still writes a stray "Activated" row, and the server's state does not change.

Fix direction: mirror #656. Add an atomic InactiveUnacknowledged transition (one projector update, one client event, rows only for what changed), carry IsRestatement on first sight, and give an inactive+acked first sight a no-row path. Prove it with an end-to-end test through DriverHostActor.ForwardNativeAlarm, and with the opc-plc respawn live check from hw3 (scratchpad recipe in the #656 comment).

Found in the #656 review (batch hw3). Pre-existing, outside #656's scope. 1. After an OpcUaClient driver respawn, every **inactive+unacked** condition the upstream replays goes through Raise→Clear (`OpcUaClientDriver.cs` ~2114-2116). That writes a duplicate "Activated" + "Cleared" `/alerts` pair, and clients briefly see the condition Active again. 2. An OpcUaClient first-sight **inactive+acked** event (`OpcUaClientDriver.cs` ~1946-1955) still writes a stray "Activated" row, and the server's state does not change. Fix direction: mirror #656. Add an atomic `InactiveUnacknowledged` transition (one projector update, one client event, rows only for what changed), carry `IsRestatement` on first sight, and give an inactive+acked first sight a no-row path. Prove it with an end-to-end test through `DriverHostActor.ForwardNativeAlarm`, and with the opc-plc respawn live check from hw3 (`scratchpad` recipe in the #656 comment).
Author
Owner

Extra detail from #658 (closed as a duplicate of item 2): the first-sight inactive+acked event is emitted as AlarmTransitionKind.Unspecified, not a flagged Clear. So (a) a host condition retained Active across the respawn stays Active — nothing clears it — and (b) with no active prior the projector's default branch writes an "Activated" row for an inactive alarm. Expected: a flagged (IsRestatement) Clear — no row when the host condition is already inactive, a real Clear when it was retained Active. Note: since #611 the projector also has a third no-row rule (flagged Unspecified moves message/severity without a row), which OpcUaClient first-sight-after-respawn can reach; keep it consistent when fixing this. Live recipe: see the #656 comment.

Extra detail from #658 (closed as a duplicate of item 2): the first-sight inactive+acked event is emitted as `AlarmTransitionKind.Unspecified`, not a flagged `Clear`. So (a) a host condition retained **Active** across the respawn stays Active — nothing clears it — and (b) with no active prior the projector's default branch writes an "Activated" row for an inactive alarm. Expected: a flagged (`IsRestatement`) Clear — no row when the host condition is already inactive, a real Clear when it was retained Active. Note: since #611 the projector also has a third no-row rule (flagged `Unspecified` moves message/severity without a row), which OpcUaClient first-sight-after-respawn can reach; keep it consistent when fixing this. Live recipe: see the #656 comment.
dohertj2 referenced this issue from a commit 2026-10-09 07:19:56 -04:00
Author
Owner

Fixed and pushed in master ea220363 (merge ea220363, branch commits 48a80b59, 24eed8ad).

  1. Respawn replay of inactive+unacked: new AlarmTransitionKind.InactiveUnacknowledged moves a condition to inactive+unacked in ONE projection, writing rows only for what changed (Activated+Cleared from an acked prior, Cleared from Active+unacked, nothing if already inactive+unacked). OpcUaClient uses it instead of Raise/Retrigger+Clear, so clients no longer see an Active flicker and respawns no longer duplicate rows.
  2. First sight of inactive+acked (incl. #658): now a flagged Clear + flagged Acknowledge instead of Unspecified; a flagged Acknowledge on an already-acked condition writes no row. A host that retained the condition Active now gets a real Cleared.

Galaxy/FOCAS/MTConnect/AbCip paths unchanged (they never emit the new kind or a flagged Ack). 4 new E2E tests through DriverHostActor.ForwardNativeAlarm (all fail with either the projector or driver change reverted).

Live gate (docker-dev, chained via central's mirror of opc-plc NorthMotor/Yellow, held inactive+unacked): old image — every respawn showed a brief Active and wrote a duplicate Activated+Cleared pair (3/3); new image — 4 respawns, no Active event, no rows; a real re-activation still wrote its Activated row; retained-Active variant wrote exactly one Cleared. Item 2 is E2E-only (a Part 9 server never replays inactive+acked on refresh). Independent Opus review: MERGE. Full suite 13214 passed / 0 failed.

Residual (pre-existing, documented in AlarmTracking.md): a cold host treats an unseen condition as acked, so after a node restart each upstream inactive+unacked condition still writes one Activated+Cleared pair. Separate bug found during the gate filed as #662 (alarm feed not restored on a non-respawning deploy / reconcile).

Fixed and pushed in master `ea220363` (merge `ea220363`, branch commits `48a80b59`, `24eed8ad`). 1. **Respawn replay of inactive+unacked:** new `AlarmTransitionKind.InactiveUnacknowledged` moves a condition to inactive+unacked in ONE projection, writing rows only for what changed (Activated+Cleared from an acked prior, Cleared from Active+unacked, nothing if already inactive+unacked). OpcUaClient uses it instead of Raise/Retrigger+Clear, so clients no longer see an Active flicker and respawns no longer duplicate rows. 2. **First sight of inactive+acked (incl. #658):** now a flagged Clear + flagged Acknowledge instead of `Unspecified`; a flagged Acknowledge on an already-acked condition writes no row. A host that retained the condition Active now gets a real Cleared. Galaxy/FOCAS/MTConnect/AbCip paths unchanged (they never emit the new kind or a flagged Ack). 4 new E2E tests through `DriverHostActor.ForwardNativeAlarm` (all fail with either the projector or driver change reverted). **Live gate (docker-dev, chained via central's mirror of opc-plc NorthMotor/Yellow, held inactive+unacked):** old image — every respawn showed a brief Active and wrote a duplicate Activated+Cleared pair (3/3); new image — 4 respawns, no Active event, no rows; a real re-activation still wrote its Activated row; retained-Active variant wrote exactly one Cleared. Item 2 is E2E-only (a Part 9 server never replays inactive+acked on refresh). Independent Opus review: MERGE. Full suite 13214 passed / 0 failed. Residual (pre-existing, documented in AlarmTracking.md): a cold host treats an unseen condition as acked, so after a node restart each upstream inactive+unacked condition still writes one Activated+Cleared pair. Separate bug found during the gate filed as #662 (alarm feed not restored on a non-respawning deploy / reconcile).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#657