Files
mxaccessgw/docs/AlarmProbeFindings.md
T
Joseph Doherty 540a1188aa docs(alarms): stop describing the wnwrap ack as working, and say the re-raise replaces the record
Follow-up to 90331b6. Three comment/prose corrections, no behaviour change.

AcknowledgeByName's comment still said the 6-arg overload "works and reaches the
alarm-history path correctly", which the same commit's own findings contradict in
three other places. It now says what was observed: rc=0 means accepted, not
applied — the 2026-08-18 probe acked a live alarm six ways and the snapshot, the
OPERATOR_NAME field, and the extension's .Acked attribute all stayed put. The -55
tracks the consumer, not the overload. Subscribe's comment gets the same
treatment: "lets AlarmAckByName succeed" becomes "return rc=0".

AlarmProbeFindings.md said the re-raise arrives as "a separate record" alongside
the returned one, which reads as coexistence and is wrong. The snapshot carries
one record per tag: the cap=1024 replies bracketing the re-raise are both
elementCount=3 (one per TestMachine_00{1,2,3}) at an identical 1613 bytes, and the
old GUID is absent from the later one. The re-raise replaces the record, so a
single poll spanning it sees the Clear and the Raise together.

Worker diff verified strictly comment-only; builds x86 on windev, 0W/0E.
2026-08-18 05:46:35 -04:00

15 KiB

Alarm Probe Findings

WnWrapAlarmConsumer rested on two assumptions that no unit test can settle, because both are properties of AVEVA's alarm provider rather than of our code. The second is now settled and one half of the first is; the questions are stated here as they were asked:

  1. GUID identity. The snapshot diff in ComputeTransitions keys on the alarm record's GUID. If wnwrap mints a fresh GUID when an alarm changes state, a single UNACK_ALM → ACK_ALM transition reads as one alarm disappearing and a different one appearing — a spurious clear plus a spurious raise on every acknowledge.
  2. ALARM_RECORDS/@COUNT semantics. IsTruncatedFetch treats a reply holding exactly maxAlmCnt records as truncated, because GetXmlCurrentAlarms2 exposes no explicit "more available" flag. If the reply's COUNT attribute carries the total active count rather than the records-in-reply count, truncation detection can become exact instead of conservative, and the bounded staleness ApplySnapshotUpdate accepts goes away.

This document records what live probe runs against the dev rig (DESKTOP-6JL3KKO, 2026-08-17 and 2026-08-18) could and could not establish, so the next attempt starts from the blocker rather than rediscovering it.

Outcome

Question Status
GUID stable across polls and ALM → RTN Answered — yes (2026-05-01 capture in AlarmClientDiscovery.md, reconfirmed 2026-08-18)
GUID stable across clear-then-re-raise Answered 2026-08-18 — no, a re-raise mints a new GUID
GUID stable across UNACK → ACK Open — the rig cannot be driven into an acknowledged state at all
COUNT = total active vs records-in-reply under a capped fetch Answered 2026-08-18 — records in the reply

The 2026-08-17 run below is kept because it is the record of the wrong-verb blocker. The 2026-08-18 run cleared that blocker with AuthenticateUser + WriteSecured and answered two of the three questions; the acknowledge leg is now blocked on something narrower and different, described in "Second attempt".

First attempt (2026-08-17): plain Write

Why The Rig Could Not Raise An Alarm

The rig is otherwise healthy, which is what makes the blocker specific rather than a general "nothing works":

  • aaEngine, alarmmgr, NmxSvc, and wnwrapServerEx are all running.
  • TestArea (area of TestMachine_001_003) and the objects themselves are deployed (deployed_version non-null in the ZB Galaxy Repository) and on scan — the probe's advised ScanState subtags report true, and every advised alarm attribute delivers an initial value, so the MXAccess read path is live.
  • The wnwrap consumer subscribes cleanly: InitializeConsumer, RegisterConsumer, Subscribe(\\DESKTOP-6JL3KKO\Galaxy!TestArea), and SetXmlAlarmQuery all return 0, and GetXmlCurrentAlarms2 returns well-formed XML on every poll.

What fails is the write that would set the alarm condition. Every Write to the alarm UDAs completes with a security failure:

WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=0 category=SecurityError detectedBy=RespondingAutomationObject detail=1008 text=]

The status comes back from the responding automation object, not from the proxy, so the request reaches the engine and the engine refuses it. The advised value confirms the refusal is total rather than transient: neither the alarm UDA nor its .InAlarm /.Acked subtags report any change after a write attempt, across six write attempts in one session (raise, clear, re-raise, cleanup). The attributes carry a security classification that a plain Write cannot satisfy.

The 2026-05-01 capture that answered the ALM → RTN leg did not hit this, because the alarm condition was driven from inside the engine by a System Platform script rather than from an external MXAccess client. That script is not running now, and the values sat idle for the whole probe session.

Unblocking

Any one of these makes both questions answerable, in rough order of cost. The second one is what the 2026-08-18 run did, and it worked:

  • Re-enable the System Platform script that flips TestMachine_001.TestAlarm001 (referenced throughout AlarmClientDiscovery.md). It writes from inside the engine, so the attribute's security classification does not apply.
  • Drive the write through AuthenticateUser + WriteSecured with a Galaxy account permitted on that classification. The worker already implements both verbs; the probe used plain Write, which is the wrong verb for a secured attribute.
  • Reclassify the test UDAs to free access in the IDE and redeploy TestMachine_001_003.

Three separate objects are wired to the same alarm UDA name, so once writes land, a maxAlmCnt of 1 or 2 forces truncation against three active alarms and answers the COUNT question in the same run.

Evidence

Snapshot payload, identical at every cap (1, 2, and 1024) and at every poll across the ~100-second session:

<?xml version="1.0"?><ALARM_RECORDS COUNT="0"></ALARM_RECORDS>

Two things follow from the empty case alone. COUNT is present on the root element in every reply, so the attribute exists as a candidate signal rather than something wnwrap omits. And COUNT agrees with the element count here — but trivially, since both are zero, which is exactly the case that cannot discriminate the two hypotheses.

The probe used for the run was a throwaway file in the windev CI clone (C:\build\mxaccessgw-ci), deleted afterwards; the clone is back to a clean tree at origin/main. Nothing in this repository changed to run it. The reusable, Skip-gated harness it was modelled on is src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs.

Second attempt (2026-08-18): secured-write verb

The findings above named the fix in their own "Unblocking" list, and it holds: AuthenticateUser + WriteSecured raises the alarms that plain Write could not touch. The rig's alarm attributes are not unreachable — they are secured, and the 2026-08-17 probe used the wrong verb.

Method

Three throwaway probes in the windev CI clone (C:\build\mxaccessgw-ci, detached at ab3ff16), each an x86 net48 xUnit fact running on a dedicated STA with a PeekMessage/DispatchMessage pump so MXAccess events deliver:

  1. secured write + both GUID legs + the capped-fetch question,
  2. the acknowledge surface, exhaustively,
  3. the acknowledge question re-asked through an independent observer.

All three used LMXProxyServerClass directly for the write path and a wwAlarmConsumerClass pair — a reader with SetXmlAlarmQuery applied and an ack-only consumer without it — mirroring WnWrapAlarmConsumer's own two-consumer arrangement. The subscription was \\DESKTOP-6JL3KKO\Galaxy!TestArea, matching the 2026-08-17 run. MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER / _PASSWORD are set at neither machine nor user scope on the box, so the probes used the default rig identity, Administrator with an empty password. All three files were deleted afterwards and the clone is a clean tree.

The secured write lands

Same session, same item, one after the other — the control reproduces 2026-08-17 exactly and the secured verb succeeds:

Write 'TestMachine_001.TestAlarm001' <- True
WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=0 category=SecurityError detectedBy=RespondingAutomationObject detail=1008 text=]
  observed 'TestMachine_001.TestAlarm001' = False

AuthenticateUser -> userId=1
WriteSecured 'TestMachine_001.TestAlarm001' <- True currentUserId=1 verifierUserId=0
DATA-CHANGE hItem=1 value=True quality=192
WRITE-COMPLETE hLMX=1 hItem=1 statuses=[success=-1 category=Ok detectedBy=RespondingAutomationObject detail=0 text=]
  observed 'TestMachine_001.TestAlarm001' = True

AuthenticateUser("Administrator", "") resolves to user id 1, matching the value WorkerLiveMxAccessSmokeTests records. No verifier is needed: verifierUserId=0 is accepted. All six subsequent raise/clear writes across the three objects behaved the same way, so the path is reliable rather than a one-off.

Q2 — ALARM_RECORDS/@COUNT reports the reply, not the total

With all three TestMachine_00{1,2,3}.TestAlarm001 alarms active, the same subscription fetched at three caps back to back:

FETCH three-active cap=1024   ALARM_RECORDS/@COUNT=3  elementCount=3
FETCH three-active cap=1      ALARM_RECORDS/@COUNT=1  elementCount=1
FETCH three-active cap=2      ALARM_RECORDS/@COUNT=2  elementCount=2

@COUNT tracked the cap, not the population. It equals the element count in every reply, including the two that are provably truncated — the galaxy held three active alarms while @COUNT read 1 and 2. The attribute therefore carries no "more available" information, and the hypothesis that it might report the total is refuted rather than merely untested.

IsTruncatedFetch stays exactly as written, and this is now settled rather than deferred: there is no exact truncation signal to switch to. Tightening it is not a future task. The conservative rule keeps its original justification — at the cap, treating a complete fetch as truncated costs one poll of staleness, while treating a truncated fetch as complete broadcasts clears for every alarm past the cap.

Q1 — clear-then-re-raise mints a new GUID

TestMachine_001.TestAlarm001 was raised, cleared, and re-raised through WriteSecured, with a full snapshot fetch after each leg:

after-raise      TAG=TestMachine_001.TestAlarm001 GUID=72B84A6E7BA74D42B611EB5393626F79 STATE=UNACK_ALM  VALUE=true
after-clear-001  TAG=TestMachine_001.TestAlarm001 GUID=72B84A6E7BA74D42B611EB5393626F79 STATE=UNACK_RTN  VALUE=false
after-reraise    TAG=TestMachine_001.TestAlarm001 GUID=2394FEAA94774154AAA97C3AC29CA51E STATE=UNACK_ALM  VALUE=true

Two facts, one confirming and one new. The ALM → RTN leg holds its GUID, reconfirming the 2026-05-01 capture against a write-driven rather than script-driven transition. The re-raise does not: the record comes back under a GUID that has never been seen before.

The two records do not coexist — the snapshot carries one record per tag, and the re-raise replaces the returned record rather than joining it. Both fetches above are the full cap=1024 reply, byte-identical in length (1613) at @COUNT=3/elementCount=3, the three records being one per TestMachine_00{1,2,3}; 72B84A6E… is simply absent from the later one. So a single poll spanning the re-raise sees the old GUID disappear and the new one appear together.

That is the correct reading for ComputeTransitions rather than a problem for it. The old GUID leaving the active set is a real Clear and the new GUID is a real Raise, because they are two alarm instances. The diff needed no change.

Q1 — the acknowledge leg is still unobserved, for a new reason

The blocker moved. The rig can now be driven into an alarm; it cannot be driven out of UNACK_ALM. Every acknowledge surface wwAlarmConsumerClass exposes was tried against a freshly raised alarm, each followed by 16 seconds of polling:

Attempt Result
AlarmAckByName 6-arg on the ack-only consumer, node = machine rc=0, state unchanged
same, node empty rc=0, state unchanged
same, name as Galaxy!TestArea.TestMachine_001.TestAlarm001 rc=0, state unchanged
same, operator name Administrator rc=0, state unchanged
AlarmAckByName 6-arg on the reader consumer (SetXmlAlarmQuery applied) rc=-55, state unchanged
AlarmAckByName 8-arg (v2) on the ack-only consumer rc=0, state unchanged

The rc=-55 on the SetXmlAlarmQuery-applied consumer reproduces the 2026-05-01 finding that motivated the two-consumer split, so the ack-only consumer was correctly provisioned; the rc=0 returns are the ones that go nowhere.

A third probe removed the last doubt by watching the alarm extension's own .Acked attribute over MXAccess — an observer entirely independent of the wnwrap snapshot:

VALUES after-raise:        'TestMachine_001.TestAlarm001.Acked' = False
AlarmAckByName -> rc=0
VALUES after-wnwrap-ack:   'TestMachine_001.TestAlarm001.Acked' = False
XML   after-wnwrap-ack:    STATE=UNACK_ALM OPRNAME= OPRNODE=

Nothing moves: not the extension attribute, not the snapshot STATE, not OPERATOR_NAME. rc=0 from wnwrap means the call was accepted, not that an acknowledgement was applied.

Acknowledging by writing the extension attribute directly is not an alternative — the attribute is not writable, and says so with an operational rather than a security failure, for both verbs:

WriteSecured 'TestMachine_001.TestAlarm001.Acked' <- true
WRITE-COMPLETE statuses=[success=0 category=MxCategoryOperationalError detectedBy=MxSourceRespondingAutomationObject detail=1007]
Write        'TestMachine_001.TestAlarm001.Acked' <- true
WRITE-COMPLETE statuses=[success=0 category=MxCategoryOperationalError detectedBy=MxSourceRespondingAutomationObject detail=1007]

detail=1007 from the responding automation object, unchanged by authentication, is a read-only attribute rather than a permission refusal — the 1008 SecurityError the alarm UDA itself returns is what a permission refusal looks like on this rig. So the ack has no MXAccess-side entry point and the wnwrap-side entry point is inert.

Remaining unblock paths for the acknowledge leg

  • Acknowledge from the System Platform side — the IDE's alarm client, InTouch, or an ArchestrA graphic bound to the alarm — and watch the snapshot from a running probe. This proves whether wnwrap's STATE ever reports ACK_ALM at all, which is the actual question; the ack API being inert may be a wnwrap defect layered on top of a snapshot that would report the state correctly.
  • Check whether alarmmgr on this rig is configured with an alarm-acknowledgement security requirement that the wnwrap consumer, which passes an operator name string and no authenticated identity, cannot meet. If so, ack over wnwrap is not merely untested here but unavailable by configuration, and the gateway's AcknowledgeByName path needs the same treatment on any customer galaxy configured that way.
  • If neither lands, the acknowledge leg stays assumed. It is worth restating that this is a documentation gap, not a correctness one: a re-minted GUID on acknowledge would produce a spurious Clear plus a spurious Raise, which is the same shape the now-observed re-raise behaviour produces and which ComputeTransitions already handles as two instances.

Rig state left behind

The three TestMachine_00{1,2,3}.TestAlarm001 UDAs are back to false and their .InAlarm subtags read false, but each leaves a UNACK_RTN record in the wnwrap snapshot, since nothing can acknowledge them away. SnapshotActiveAlarms counts only UNACK_ALM and ACK_ALM as active, so these are inert for the gateway; they will clear on the next alarmmgr restart.