From c3c603f169baad1590985b860b08a7ff1682468a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 18 Aug 2026 06:09:06 -0400 Subject: [PATCH] docs(alarms): describe the real StreamAlarms open sequence, and close TST-16 The final integration review's non-blocker reservations, all documentation or comment truth except one test arm. The alarm feed opens provider_status -> snapshot_status -> cached active_alarm -> snapshot_complete, which is what GatewayAlarmMonitor has done since the snapshot_status frame landed. Two places still described the old order: docs/Grpc.md said provider_status arrived *after* the initial snapshot, contradicting its own snapshot_status section two paragraphs down, and AlarmFeedMessage's leading proto comment named neither status frame at all. Both now state the sequence the monitor emits, so a client author reading either one gets the frame order right. The proto comment change flows through the generated trees (Contracts, Go, Java) and the client descriptor set; the Rust vendored copy stays byte-identical to canonical. Python's generator does not carry proto comments into its output, so it has no delta. AlarmsHubPublisherTests' valueless-payload case covered snapshot_complete and provider_status but not snapshot_status, leaving the newest arm unpinned against the redaction switch that must ignore it. Added. WnWrapAlarmConsumer's ack comment led with the 2026-05-01 reading that -55 tracks the 8-arg overload, then refuted itself six lines later with the 2026-08-18 probe. It now leads with the observation labelled as narrower than it reads -- mirroring the correction already in docs/AlarmClientDiscovery.md -- so the block argues one thing: the 6-arg call site stays for parity, and rc semantics are per the probe. A paragraph orphaned by an earlier splice is rewrapped. Comment interior only; the file compiles on Windows. TST-16 gets a dated closure note rather than a rewrite: the flag it called dead was implemented 2026-08-18. GatewayDashboardDesign's /browse paragraph gains the failed-read carve-out GatewayConfiguration already documented, so the two agree that a failed read keeps its - placeholder. --- .../remediation/60-testing-docs-gaps.md | 2 + .../internal/generated/mxaccess_gateway.pb.go | 10 +++-- .../mxaccess_gateway/v1/MxaccessGateway.java | 20 ++++++--- .../descriptors/mxaccessgw-client-v1.protoset | Bin 122424 -> 122737 bytes clients/rust/protos/mxaccess_gateway.proto | 10 +++-- docs/GatewayDashboardDesign.md | 4 +- docs/Grpc.md | 9 ++-- .../Generated/MxaccessGateway.cs | 10 +++-- .../Protos/mxaccess_gateway.proto | 10 +++-- .../Dashboard/AlarmsHubPublisherTests.cs | 10 ++++- .../MxAccess/WnWrapAlarmConsumer.cs | 39 ++++++++++-------- 11 files changed, 83 insertions(+), 41 deletions(-) diff --git a/archreview/remediation/60-testing-docs-gaps.md b/archreview/remediation/60-testing-docs-gaps.md index f376539..6ebf9cd 100644 --- a/archreview/remediation/60-testing-docs-gaps.md +++ b/archreview/remediation/60-testing-docs-gaps.md @@ -355,6 +355,8 @@ If TST-02's interim mitigation (flip retention off) is chosen instead of impleme **Verification.** `dotnet build src/ZB.MOM.WW.MxGateway.Server`; test toggling the flag suppresses/shows values; docs match. +**Closed 2026-08-18 — implemented (the Design's recommended path).** The flag is no longer dead: it now gates value display on all three seams that carry a tag value. `Dashboard/DashboardLiveDataService.cs` substitutes `[redacted]` at the service boundary for each successfully read `/browse` tag (a failed read keeps its `-` placeholder, so the error row and the redaction cannot contradict each other); `Dashboard/Hubs/AlarmsHubPublisher.cs` clears `current_value`/`limit_value` from a **deep-cloned** `AlarmFeedMessage` before broadcasting to `/hubs/alarms`, and `DashboardEventBroadcaster` does the same for the events-hub mirror — clones, because the sources fan out to gRPC `StreamAlarms`/`StreamEvents` and the replay ring, so no gRPC client is affected by this dashboard-display flag. The flag is **kept**, default still `false`. The stale prose the Finding flagged is gone: `docs/GatewayConfiguration.md` and `docs/GatewayDashboardDesign.md` now describe the real behaviour. Status tracked in `00-tracking.md` (already **Done**). + --- ## TST-17 — Vendor-gated alarm parity residuals silently lossy `Medium` · `—` diff --git a/clients/go/internal/generated/mxaccess_gateway.pb.go b/clients/go/internal/generated/mxaccess_gateway.pb.go index 4ef7d3a..99596e8 100644 --- a/clients/go/internal/generated/mxaccess_gateway.pb.go +++ b/clients/go/internal/generated/mxaccess_gateway.pb.go @@ -7342,9 +7342,13 @@ func (x *StreamAlarmsRequest) GetAlarmFilterPrefix() string { return "" } -// One message on the StreamAlarms feed. The stream opens with one -// `active_alarm` per currently-active alarm, then a single -// `snapshot_complete`, then a `transition` for every subsequent change. +// One message on the StreamAlarms feed. The stream opens with a +// `provider_status` (the current provider mode), then a `snapshot_status` +// (whether the cached active set may be incomplete), then one `active_alarm` +// per currently-active alarm, then a single `snapshot_complete`. After that +// come the live frames: a `transition` for every subsequent alarm change, +// interleaved with a further `provider_status` on each failover/failback and a +// further `snapshot_status` on each change of the truncation verdict. type AlarmFeedMessage struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Payload: diff --git a/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java b/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java index b49bf8b..153aea4 100644 --- a/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java +++ b/clients/java/src/main/generated/main/java/mxaccess_gateway/v1/MxaccessGateway.java @@ -89203,9 +89203,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-   * One message on the StreamAlarms feed. The stream opens with one
-   * `active_alarm` per currently-active alarm, then a single
-   * `snapshot_complete`, then a `transition` for every subsequent change.
+   * One message on the StreamAlarms feed. The stream opens with a
+   * `provider_status` (the current provider mode), then a `snapshot_status`
+   * (whether the cached active set may be incomplete), then one `active_alarm`
+   * per currently-active alarm, then a single `snapshot_complete`. After that
+   * come the live frames: a `transition` for every subsequent alarm change,
+   * interleaved with a further `provider_status` on each failover/failback and a
+   * further `snapshot_status` on each change of the truncation verdict.
    * 
* * Protobuf type {@code mxaccess_gateway.v1.AlarmFeedMessage} @@ -89739,9 +89743,13 @@ public final class MxaccessGateway extends com.google.protobuf.GeneratedFile { } /** *
-     * One message on the StreamAlarms feed. The stream opens with one
-     * `active_alarm` per currently-active alarm, then a single
-     * `snapshot_complete`, then a `transition` for every subsequent change.
+     * One message on the StreamAlarms feed. The stream opens with a
+     * `provider_status` (the current provider mode), then a `snapshot_status`
+     * (whether the cached active set may be incomplete), then one `active_alarm`
+     * per currently-active alarm, then a single `snapshot_complete`. After that
+     * come the live frames: a `transition` for every subsequent alarm change,
+     * interleaved with a further `provider_status` on each failover/failback and a
+     * further `snapshot_status` on each change of the truncation verdict.
      * 
* * Protobuf type {@code mxaccess_gateway.v1.AlarmFeedMessage} diff --git a/clients/proto/descriptors/mxaccessgw-client-v1.protoset b/clients/proto/descriptors/mxaccessgw-client-v1.protoset index 30a8c97c340245ff050d2dc488b98ae93ab7cfd7..c92518c68a038b07015e508d549be769b01dfcb6 100644 GIT binary patch delta 7369 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 AZU6uP delta 7036 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 diff --git a/clients/rust/protos/mxaccess_gateway.proto b/clients/rust/protos/mxaccess_gateway.proto index b5e2a6b..daa3e8d 100644 --- a/clients/rust/protos/mxaccess_gateway.proto +++ b/clients/rust/protos/mxaccess_gateway.proto @@ -1003,9 +1003,13 @@ message StreamAlarmsRequest { string alarm_filter_prefix = 2; } -// One message on the StreamAlarms feed. The stream opens with one -// `active_alarm` per currently-active alarm, then a single -// `snapshot_complete`, then a `transition` for every subsequent change. +// One message on the StreamAlarms feed. The stream opens with a +// `provider_status` (the current provider mode), then a `snapshot_status` +// (whether the cached active set may be incomplete), then one `active_alarm` +// per currently-active alarm, then a single `snapshot_complete`. After that +// come the live frames: a `transition` for every subsequent alarm change, +// interleaved with a further `provider_status` on each failover/failback and a +// further `snapshot_status` on each change of the truncation verdict. message AlarmFeedMessage { oneof payload { // Part of the initial active-alarm snapshot (ConditionRefresh). diff --git a/docs/GatewayDashboardDesign.md b/docs/GatewayDashboardDesign.md index bfc7fdb..f69b8c0 100644 --- a/docs/GatewayDashboardDesign.md +++ b/docs/GatewayDashboardDesign.md @@ -520,7 +520,9 @@ panel. The panel shows each subscribed tag's live value, MXAccess data type, quality and source timestamp, refreshed every two seconds — but the value column obeys `Dashboard:ShowTagValues` like every other dashboard value surface. With the flag false (the default) `DashboardLiveDataService` hands the page -`[redacted]` in place of the formatted value; data type, quality, source +`[redacted]` in place of the formatted value of each **successfully read** tag — +a failed read keeps its `-` placeholder, since there was no value to suppress — +while data type, quality, source timestamp and any read error are untouched, so the panel still answers "is this tag advising and healthy" without disclosing the value. The substitution happens at the service, not in the page: one decision point, and the value never enters diff --git a/docs/Grpc.md b/docs/Grpc.md index 15bd1c8..6d7794e 100644 --- a/docs/Grpc.md +++ b/docs/Grpc.md @@ -114,10 +114,11 @@ message AlarmProviderStatus { } ``` -The gateway emits `provider_status` once when a client first subscribes -(immediately after the initial snapshot and before the first live transition) -and again on every failover or failback. A late-joining client therefore -always learns the current provider mode without waiting for the next switch. +The gateway emits `provider_status` once when a client first subscribes — as +the **first** frame on the stream, before the `snapshot_status` frame and +before any cached `active_alarm` — and again on every failover or failback. A +late-joining client therefore learns the current provider mode before it sees a +single alarm, without waiting for the next switch. #### Snapshot completeness on the alarm feed diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs index d171283..b5d06b2 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs @@ -28513,9 +28513,13 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto { } /// - /// One message on the StreamAlarms feed. The stream opens with one - /// `active_alarm` per currently-active alarm, then a single - /// `snapshot_complete`, then a `transition` for every subsequent change. + /// One message on the StreamAlarms feed. The stream opens with a + /// `provider_status` (the current provider mode), then a `snapshot_status` + /// (whether the cached active set may be incomplete), then one `active_alarm` + /// per currently-active alarm, then a single `snapshot_complete`. After that + /// come the live frames: a `transition` for every subsequent alarm change, + /// interleaved with a further `provider_status` on each failover/failback and a + /// further `snapshot_status` on each change of the truncation verdict. /// [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class AlarmFeedMessage : pb::IMessage diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto index b5e2a6b..daa3e8d 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto @@ -1003,9 +1003,13 @@ message StreamAlarmsRequest { string alarm_filter_prefix = 2; } -// One message on the StreamAlarms feed. The stream opens with one -// `active_alarm` per currently-active alarm, then a single -// `snapshot_complete`, then a `transition` for every subsequent change. +// One message on the StreamAlarms feed. The stream opens with a +// `provider_status` (the current provider mode), then a `snapshot_status` +// (whether the cached active set may be incomplete), then one `active_alarm` +// per currently-active alarm, then a single `snapshot_complete`. After that +// come the live frames: a `transition` for every subsequent alarm change, +// interleaved with a further `provider_status` on each failover/failback and a +// further `snapshot_status` on each change of the truncation verdict. message AlarmFeedMessage { oneof payload { // Part of the initial active-alarm snapshot (ConditionRefresh). diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/AlarmsHubPublisherTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/AlarmsHubPublisherTests.cs index c5f0394..7eb18b3 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/AlarmsHubPublisherTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/AlarmsHubPublisherTests.cs @@ -119,13 +119,21 @@ public sealed class AlarmsHubPublisherTests }, }; + AlarmFeedMessage snapshotStatus = new() + { + SnapshotStatus = new AlarmSnapshotStatus { Truncated = true }, + }; + CapturingHubContext hubContext = await RunPublisherAsync( showTagValues: false, snapshotComplete, - providerStatus); + providerStatus, + snapshotStatus); Assert.Same(snapshotComplete, hubContext.Sent[0]); Assert.Same(providerStatus, hubContext.Sent[1]); + Assert.Same(snapshotStatus, hubContext.Sent[2]); + Assert.True(hubContext.Sent[2].SnapshotStatus.Truncated); } /// diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs index 94e51f7..55bf215 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs @@ -345,23 +345,28 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer ?? throw new InvalidOperationException( "Cannot acknowledge: WnWrapAlarmConsumer was disposed or has not been subscribed yet."); - // Empirically (live dev-rig 2026-05-01): the IwwAlarmConsumer2 - // 8-arg AlarmAckByName returns -55 on this AVEVA build (looks like - // a stub) and the legacy 6-arg IwwAlarmConsumer.AlarmAckByName - // returns 0, which is why the 6-arg overload is the one called here. - // A zero return is NOT evidence the acknowledgement was applied: the - // 2026-08-18 probe acked a real, freshly raised alarm six ways and - // watched the snapshot stay UNACK_ALM, OPERATOR_NAME stay empty, and - // the alarm extension's own .Acked attribute stay False for 16 s after - // each call. On that rig the ack is accepted and then inert, and the - // -55 tracks the consumer rather than the overload — both overloads - // return -55 against a SetXmlAlarmQuery-applied consumer and 0 against - // the ack-only one. Whether any wnwrap ack reaches the alarm-history - // path is therefore unverified; see docs/AlarmProbeFindings.md. - // Operator-domain - // and operator-full-name fields are accepted by the proto contract - // for forward-compat but are not propagated to AVEVA today — - // wrapped in the 6-arg call so domain/full-name go to the + // The original observation (live dev-rig 2026-05-01) was narrower than + // it read: the IwwAlarmConsumer2 8-arg AlarmAckByName returned -55 + // "and looked like a stub" while the legacy 6-arg + // IwwAlarmConsumer.AlarmAckByName returned 0, which is how the 6-arg + // overload came to be the one called here. The 2026-08-18 probe + // corrected both halves. The -55 tracks the *consumer*, not the + // overload: both overloads return -55 against a + // SetXmlAlarmQuery-applied consumer and 0 against the ack-only one + // used above. And a zero return is NOT evidence the acknowledgement + // was applied — the probe acked a real, freshly raised alarm six ways + // and watched the snapshot stay UNACK_ALM, OPERATOR_NAME stay empty, + // and the alarm extension's own .Acked attribute stay False for 16 s + // after each call. On that rig the ack is accepted and then inert, so + // whether any wnwrap ack reaches the alarm-history path is + // unverified; see docs/AlarmProbeFindings.md and the 2026-08-18 + // correction in docs/AlarmClientDiscovery.md. The 6-arg call site + // below therefore stays as-is for MXAccess parity — the choice is no + // longer justified by the rc, and rc semantics are per the probe. + // + // Operator-domain and operator-full-name fields are accepted by the + // proto contract for forward-compat but are not propagated to AVEVA + // today — wrapped in the 6-arg call so domain/full-name go to the // alarm-history operator-name field via the szOprName parameter. // Suppress unused-warning explicitly: _ = ackOperatorDomain;