feat(alarms): feed-level snapshot_status truncation frame on StreamAlarms

This commit is contained in:
Joseph Doherty
2026-08-17 07:16:51 -04:00
parent 094f2ffee4
commit fccf75324b
8 changed files with 920 additions and 197 deletions
+18 -1
View File
@@ -203,7 +203,7 @@ Consequences, and how this sits with the existing failover/reconcile design:
separately — see the next decision. A galaxy that truncates persistently
is a configuration problem: raise `MxGateway:Alarms:MaxAlarmsPerFetch`.
### Alarms — truncation is reported per record on the public snapshot stream
### Alarms — truncation is reported per record on the snapshot stream, as a status frame on the live feed
Decision (2026-08-17): the truncated-fetch verdict above is carried to clients as
`QueryActiveAlarmsReplyPayload.snapshot_truncated` on the worker IPC reply and as
@@ -239,6 +239,23 @@ sub-cap fetch clears it, and `GatewayAlarmMonitor.ClearCache` drops it with the
cache generation it describes. A caveat that never turns off is a caveat
operators learn to ignore.
The **live feed carries the verdict as set-level status**, not per record.
`StreamAlarms` has an envelope — `AlarmFeedMessage` — so the shape forced on
`QueryActiveAlarms` above is not forced here: the feed gets a fifth oneof case,
`snapshot_status` (`AlarmSnapshotStatus.truncated`), alongside `provider_status`.
The two carriers are therefore deliberately different shapes for the same verdict,
and each is the only additive option on its own surface. Emission is
**edge-triggered**, for the same reason the flag is not latched: a status frame
repeated on every reconcile is noise a consumer filters out, and a filtered-out
signal is no signal. The exception is the open-time frame, which is
unconditional — a late joiner cannot distinguish "not truncated" from "this
gateway does not send the frame" by silence, so it is told explicitly. It is
ordered after `provider_status` and before the cached `active_alarm` frames so a
consumer applying the snapshot as it streams holds the caveat while it applies
the records it qualifies. A monitor restart's `ClearCache` emits the clearing
frame as well: feed subscribers outlive the monitor's worker session, so a silent
re-seed would leave them caveating a set that is no longer truncated.
This is gateway metadata about **our** fetch mechanics, not a claim about MXAccess
behaviour, so it is not a parity deviation: no event is synthesized and no
MXAccess-observable semantics change.
+31 -1
View File
@@ -94,7 +94,7 @@ An accepted gRPC command payload can still be too large for the worker pipe: the
### `StreamAlarms`
`StreamAlarms` is a server-streaming, **session-less** RPC that attaches to the gateway's central alarm feed. The handler delegates to `IGatewayAlarmService.StreamAsync`. The stream opens with one `AlarmFeedMessage` carrying an `active_alarm` per currently-active alarm (the ConditionRefresh snapshot), then a single `snapshot_complete`, then a `transition` for every subsequent raise / acknowledge / clear. It is served by the always-on `GatewayAlarmMonitor`, which owns a single gateway-managed worker session and fans out to every attached client — clients no longer open a session of their own. `alarm_filter_prefix`, when set, scopes the stream to a sub-tree.
`StreamAlarms` is a server-streaming, **session-less** RPC that attaches to the gateway's central alarm feed. The handler delegates to `IGatewayAlarmService.StreamAsync`. The stream opens with a `provider_status` and a `snapshot_status` `AlarmFeedMessage` (the current provider mode and snapshot-completeness verdict), then one `AlarmFeedMessage` carrying an `active_alarm` per currently-active alarm (the ConditionRefresh snapshot), then a single `snapshot_complete`, then a `transition` for every subsequent raise / acknowledge / clear — interleaved with a further `provider_status` on each failover/failback and a further `snapshot_status` on each change of the truncation verdict. It is served by the always-on `GatewayAlarmMonitor`, which owns a single gateway-managed worker session and fans out to every attached client — clients no longer open a session of their own. `alarm_filter_prefix`, when set, scopes the stream to a sub-tree.
### `QueryActiveAlarms`
@@ -119,6 +119,36 @@ The gateway emits `provider_status` once when a client first subscribes
and again on every failover or failback. A late-joining client therefore
always learns the current provider mode without waiting for the next switch.
#### Snapshot completeness on the alarm feed
`AlarmFeedMessage` has a fifth `payload` case, `snapshot_status`, carrying
an `AlarmSnapshotStatus` message:
```protobuf
message AlarmSnapshotStatus {
bool truncated = 1; // the cached active set may be missing alarms
}
```
It is the feed-level twin of the per-record
`ActiveAlarmSnapshot.from_truncated_snapshot` flag: `truncated` is true while the
monitor's cached active-alarm set derives from a capped worker fetch. Read it as
"this set may be incomplete", never as a statement about record fidelity — that
is what `degraded` / `source_provider` mean, and the two are independent.
Emission mirrors `provider_status` but with one ordering rule of its own. The
gateway emits `snapshot_status` once when a client subscribes, **after** the
open-time `provider_status` and **before** the cached `active_alarm` frames, so a
consumer applying the snapshot as it streams has the completeness caveat in hand
while it applies it. The open-time frame is unconditional — an explicit
`truncated = false` is what distinguishes a complete set from a gateway that
never sends the frame. Afterwards it is emitted only on a *change* of verdict:
when a reconcile flips the verdict either way, and when a monitor restart drops a
truncated verdict with the cache generation it describes (feed subscribers
outlive the monitor's worker session, so they see that clear). Clients that do
not know the case see an unset `payload` oneof and ignore the message, exactly as
before.
`AlarmProviderMode` is an enum with three values:
| Value | Meaning |
+12 -1
View File
@@ -249,7 +249,18 @@ from broadcasting Clears for alarms it simply had no room to mention. That
suppression is reported structurally rather than only in a rate-limited worker
warning: the `QueryActiveAlarms` reply payload carries `snapshot_truncated`,
every `ActiveAlarmSnapshot` in it carries `from_truncated_snapshot`, and the
dashboard Alarms tab shows a warning banner while the flag is set. The flag
dashboard Alarms tab shows a warning banner while the flag is set. The live
`StreamAlarms` feed carries the same verdict at feed level as an
`AlarmSnapshotStatus` message (the `snapshot_status` oneof case), so a streaming
consumer learns it without polling `QueryActiveAlarms`. It is emitted on stream
open — after the `provider_status` frame and before the cached `active_alarm`
frames, so the caveat precedes the records it qualifies — and thereafter only on
a *change* of verdict (a status frame repeated every reconcile is one consumers
learn to ignore). The open-time frame is unconditional: an explicit
`truncated = false` is what separates a complete set from a gateway that never
sends the frame. A monitor restart drops the verdict with the cache generation it
describes, and because feed subscribers outlive the monitor's worker session that
drop is emitted as a clearing frame too. The flag
means "this active set may be incomplete", not "this record is unreliable" —
it is independent of the subtag-fallback `degraded` field above. It is not
latched: the first fetch that comes back under the cap is complete, restores
@@ -361,173 +361,176 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
"cm94eRIaChJkaWFnbm9zdGljX21lc3NhZ2UYBiABKAlCCgoIX2hyZXN1bHRK",
"BAgBEAJSCnNlc3Npb25faWQiUQoTU3RyZWFtQWxhcm1zUmVxdWVzdBIdChVj",
"bGllbnRfY29ycmVsYXRpb25faWQYASABKAkSGwoTYWxhcm1fZmlsdGVyX3By",
"ZWZpeBgCIAEoCSKEAgoQQWxhcm1GZWVkTWVzc2FnZRJACgxhY3RpdmVfYWxh",
"ZWZpeBgCIAEoCSLJAgoQQWxhcm1GZWVkTWVzc2FnZRJACgxhY3RpdmVfYWxh",
"cm0YASABKAsyKC5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFsYXJtU25h",
"cHNob3RIABIbChFzbmFwc2hvdF9jb21wbGV0ZRgCIAEoCEgAEkEKCnRyYW5z",
"aXRpb24YAyABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLk9uQWxhcm1UcmFu",
"c2l0aW9uRXZlbnRIABJDCg9wcm92aWRlcl9zdGF0dXMYBCABKAsyKC5teGFj",
"Y2Vzc19nYXRld2F5LnYxLkFsYXJtUHJvdmlkZXJTdGF0dXNIAEIJCgdwYXls",
"b2FkIpgBChNBbGFybVByb3ZpZGVyU3RhdHVzEjQKBG1vZGUYASABKA4yJi5t",
"eGFjY2Vzc19nYXRld2F5LnYxLkFsYXJtUHJvdmlkZXJNb2RlEhAKCGRlZ3Jh",
"ZGVkGAIgASgIEg4KBnJlYXNvbhgDIAEoCRIpCgVzaW5jZRgEIAEoCzIaLmdv",
"b2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAi6wEKDU14U3RhdHVzUHJveHkSDwoH",
"c3VjY2VzcxgBIAEoBRI3CghjYXRlZ29yeRgCIAEoDjIlLm14YWNjZXNzX2dh",
"dGV3YXkudjEuTXhTdGF0dXNDYXRlZ29yeRI4CgtkZXRlY3RlZF9ieRgDIAEo",
"DjIjLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNTb3VyY2USDgoGZGV0",
"YWlsGAQgASgFEhQKDHJhd19jYXRlZ29yeRgFIAEoBRIXCg9yYXdfZGV0ZWN0",
"ZWRfYnkYBiABKAUSFwoPZGlhZ25vc3RpY190ZXh0GAcgASgJIukDCgdNeFZh",
"bHVlEjIKCWRhdGFfdHlwZRgBIAEoDjIfLm14YWNjZXNzX2dhdGV3YXkudjEu",
"TXhEYXRhVHlwZRIUCgx2YXJpYW50X3R5cGUYAiABKAkSDwoHaXNfbnVsbBgD",
"IAEoCBIWCg5yYXdfZGlhZ25vc3RpYxgEIAEoCRIVCg1yYXdfZGF0YV90eXBl",
"GAUgASgFEhQKCmJvb2xfdmFsdWUYCiABKAhIABIVCgtpbnQzMl92YWx1ZRgL",
"IAEoBUgAEhUKC2ludDY0X3ZhbHVlGAwgASgDSAASFQoLZmxvYXRfdmFsdWUY",
"DSABKAJIABIWCgxkb3VibGVfdmFsdWUYDiABKAFIABIWCgxzdHJpbmdfdmFs",
"dWUYDyABKAlIABI1Cg90aW1lc3RhbXBfdmFsdWUYECABKAsyGi5nb29nbGUu",
"cHJvdG9idWYuVGltZXN0YW1wSAASMwoLYXJyYXlfdmFsdWUYESABKAsyHC5t",
"eGFjY2Vzc19nYXRld2F5LnYxLk14QXJyYXlIABITCglyYXdfdmFsdWUYEiAB",
"KAxIABJAChJzcGFyc2VfYXJyYXlfdmFsdWUYEyABKAsyIi5teGFjY2Vzc19n",
"YXRld2F5LnYxLk14U3BhcnNlQXJyYXlIAEIGCgRraW5kIv4ECgdNeEFycmF5",
"EjoKEWVsZW1lbnRfZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3NfZ2F0ZXdh",
"eS52MS5NeERhdGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRISCgpkaW1l",
"bnNpb25zGAMgAygNEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEh0KFXJhd19l",
"bGVtZW50X2RhdGFfdHlwZRgFIAEoBRI1Cgtib29sX3ZhbHVlcxgKIAEoCzIe",
"Lm14YWNjZXNzX2dhdGV3YXkudjEuQm9vbEFycmF5SAASNwoMaW50MzJfdmFs",
"dWVzGAsgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQzMkFycmF5SAAS",
"NwoMaW50NjRfdmFsdWVzGAwgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5J",
"bnQ2NEFycmF5SAASNwoMZmxvYXRfdmFsdWVzGA0gASgLMh8ubXhhY2Nlc3Nf",
"Z2F0ZXdheS52MS5GbG9hdEFycmF5SAASOQoNZG91YmxlX3ZhbHVlcxgOIAEo",
"CzIgLm14YWNjZXNzX2dhdGV3YXkudjEuRG91YmxlQXJyYXlIABI5Cg1zdHJp",
"bmdfdmFsdWVzGA8gASgLMiAubXhhY2Nlc3NfZ2F0ZXdheS52MS5TdHJpbmdB",
"cnJheUgAEj8KEHRpbWVzdGFtcF92YWx1ZXMYECABKAsyIy5teGFjY2Vzc19n",
"YXRld2F5LnYxLlRpbWVzdGFtcEFycmF5SAASMwoKcmF3X3ZhbHVlcxgRIAEo",
"CzIdLm14YWNjZXNzX2dhdGV3YXkudjEuUmF3QXJyYXlIAEIICgZ2YWx1ZXMi",
"mQEKDU14U3BhcnNlQXJyYXkSOgoRZWxlbWVudF9kYXRhX3R5cGUYASABKA4y",
"Hy5teGFjY2Vzc19nYXRld2F5LnYxLk14RGF0YVR5cGUSFAoMdG90YWxfbGVu",
"Z3RoGAIgASgNEjYKCGVsZW1lbnRzGAMgAygLMiQubXhhY2Nlc3NfZ2F0ZXdh",
"eS52MS5NeFNwYXJzZUVsZW1lbnQiTQoPTXhTcGFyc2VFbGVtZW50Eg0KBWlu",
"ZGV4GAEgASgNEisKBXZhbHVlGAIgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52",
"MS5NeFZhbHVlIhsKCUJvb2xBcnJheRIOCgZ2YWx1ZXMYASADKAgiHAoKSW50",
"MzJBcnJheRIOCgZ2YWx1ZXMYASADKAUiHAoKSW50NjRBcnJheRIOCgZ2YWx1",
"ZXMYASADKAMiHAoKRmxvYXRBcnJheRIOCgZ2YWx1ZXMYASADKAIiHQoLRG91",
"YmxlQXJyYXkSDgoGdmFsdWVzGAEgAygBIh0KC1N0cmluZ0FycmF5Eg4KBnZh",
"bHVlcxgBIAMoCSI8Cg5UaW1lc3RhbXBBcnJheRIqCgZ2YWx1ZXMYASADKAsy",
"Gi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIhoKCFJhd0FycmF5Eg4KBnZh",
"bHVlcxgBIAMoDCJYCg5Qcm90b2NvbFN0YXR1cxI1CgRjb2RlGAEgASgOMicu",
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5Qcm90b2NvbFN0YXR1c0NvZGUSDwoHbWVz",
"c2FnZRgCIAEoCSqfCwoNTXhDb21tYW5kS2luZBIfChtNWF9DT01NQU5EX0tJ",
"TkRfVU5TUEVDSUZJRUQQABIcChhNWF9DT01NQU5EX0tJTkRfUkVHSVNURVIQ",
"ARIeChpNWF9DT01NQU5EX0tJTkRfVU5SRUdJU1RFUhACEhwKGE1YX0NPTU1B",
"TkRfS0lORF9BRERfSVRFTRADEh0KGU1YX0NPTU1BTkRfS0lORF9BRERfSVRF",
"TTIQBBIfChtNWF9DT01NQU5EX0tJTkRfUkVNT1ZFX0lURU0QBRIaChZNWF9D",
"T01NQU5EX0tJTkRfQURWSVNFEAYSHQoZTVhfQ09NTUFORF9LSU5EX1VOX0FE",
"VklTRRAHEiYKIk1YX0NPTU1BTkRfS0lORF9BRFZJU0VfU1VQRVJWSVNPUlkQ",
"CBIlCiFNWF9DT01NQU5EX0tJTkRfQUREX0JVRkZFUkVEX0lURU0QCRIwCixN",
"WF9DT01NQU5EX0tJTkRfU0VUX0JVRkZFUkVEX1VQREFURV9JTlRFUlZBTBAK",
"EhsKF01YX0NPTU1BTkRfS0lORF9TVVNQRU5EEAsSHAoYTVhfQ09NTUFORF9L",
"SU5EX0FDVElWQVRFEAwSGQoVTVhfQ09NTUFORF9LSU5EX1dSSVRFEA0SGgoW",
"TVhfQ09NTUFORF9LSU5EX1dSSVRFMhAOEiEKHU1YX0NPTU1BTkRfS0lORF9X",
"UklURV9TRUNVUkVEEA8SIgoeTVhfQ09NTUFORF9LSU5EX1dSSVRFX1NFQ1VS",
"RUQyEBASJQohTVhfQ09NTUFORF9LSU5EX0FVVEhFTlRJQ0FURV9VU0VSEBES",
"KAokTVhfQ09NTUFORF9LSU5EX0FSQ0hFU1RSQV9VU0VSX1RPX0lEEBISIQod",
"TVhfQ09NTUFORF9LSU5EX0FERF9JVEVNX0JVTEsQExIkCiBNWF9DT01NQU5E",
"X0tJTkRfQURWSVNFX0lURU1fQlVMSxAUEiQKIE1YX0NPTU1BTkRfS0lORF9S",
"RU1PVkVfSVRFTV9CVUxLEBUSJwojTVhfQ09NTUFORF9LSU5EX1VOX0FEVklT",
"RV9JVEVNX0JVTEsQFhIiCh5NWF9DT01NQU5EX0tJTkRfU1VCU0NSSUJFX0JV",
"TEsQFxIkCiBNWF9DT01NQU5EX0tJTkRfVU5TVUJTQ1JJQkVfQlVMSxAYEiQK",
"IE1YX0NPTU1BTkRfS0lORF9TVUJTQ1JJQkVfQUxBUk1TEBkSJgoiTVhfQ09N",
"TUFORF9LSU5EX1VOU1VCU0NSSUJFX0FMQVJNUxAaEiUKIU1YX0NPTU1BTkRf",
"S0lORF9BQ0tOT1dMRURHRV9BTEFSTRAbEicKI01YX0NPTU1BTkRfS0lORF9R",
"VUVSWV9BQ1RJVkVfQUxBUk1TEBwSLQopTVhfQ09NTUFORF9LSU5EX0FDS05P",
"V0xFREdFX0FMQVJNX0JZX05BTUUQHRIeChpNWF9DT01NQU5EX0tJTkRfV1JJ",
"VEVfQlVMSxAeEh8KG01YX0NPTU1BTkRfS0lORF9XUklURTJfQlVMSxAfEiYK",
"Ik1YX0NPTU1BTkRfS0lORF9XUklURV9TRUNVUkVEX0JVTEsQIBInCiNNWF9D",
"T01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRDJfQlVMSxAhEh0KGU1YX0NPTU1B",
"TkRfS0lORF9SRUFEX0JVTEsQIhIYChRNWF9DT01NQU5EX0tJTkRfUElORxBk",
"EiUKIU1YX0NPTU1BTkRfS0lORF9HRVRfU0VTU0lPTl9TVEFURRBlEiMKH01Y",
"X0NPTU1BTkRfS0lORF9HRVRfV09SS0VSX0lORk8QZhIgChxNWF9DT01NQU5E",
"X0tJTkRfRFJBSU5fRVZFTlRTEGcSIwofTVhfQ09NTUFORF9LSU5EX1NIVVRE",
"T1dOX1dPUktFUhBoKnoKEUFsYXJtUHJvdmlkZXJNb2RlEiMKH0FMQVJNX1BS",
"T1ZJREVSX01PREVfVU5TUEVDSUZJRUQQABIgChxBTEFSTV9QUk9WSURFUl9N",
"T0RFX0FMQVJNTUdSEAESHgoaQUxBUk1fUFJPVklERVJfTU9ERV9TVUJUQUcQ",
"AiqtAgoNTXhFdmVudEZhbWlseRIfChtNWF9FVkVOVF9GQU1JTFlfVU5TUEVD",
"SUZJRUQQABIiCh5NWF9FVkVOVF9GQU1JTFlfT05fREFUQV9DSEFOR0UQARIl",
"CiFNWF9FVkVOVF9GQU1JTFlfT05fV1JJVEVfQ09NUExFVEUQAhImCiJNWF9F",
"VkVOVF9GQU1JTFlfT1BFUkFUSU9OX0NPTVBMRVRFEAMSKwonTVhfRVZFTlRf",
"RkFNSUxZX09OX0JVRkZFUkVEX0RBVEFfQ0hBTkdFEAQSJwojTVhfRVZFTlRf",
"RkFNSUxZX09OX0FMQVJNX1RSQU5TSVRJT04QBRIyCi5NWF9FVkVOVF9GQU1J",
"TFlfT05fQUxBUk1fUFJPVklERVJfTU9ERV9DSEFOR0VEEAYqygEKE0FsYXJt",
"VHJhbnNpdGlvbktpbmQSJQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX1VOU1BF",
"Q0lGSUVEEAASHwobQUxBUk1fVFJBTlNJVElPTl9LSU5EX1JBSVNFEAESJQoh",
"QUxBUk1fVFJBTlNJVElPTl9LSU5EX0FDS05PV0xFREdFEAISHwobQUxBUk1f",
"VFJBTlNJVElPTl9LSU5EX0NMRUFSEAMSIwofQUxBUk1fVFJBTlNJVElPTl9L",
"SU5EX1JFVFJJR0dFUhAEKqoBChNBbGFybUNvbmRpdGlvblN0YXRlEiUKIUFM",
"QVJNX0NPTkRJVElPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFMQVJNX0NP",
"TkRJVElPTl9TVEFURV9BQ1RJVkUQARImCiJBTEFSTV9DT05ESVRJT05fU1RB",
"VEVfQUNUSVZFX0FDS0VEEAISIgoeQUxBUk1fQ09ORElUSU9OX1NUQVRFX0lO",
"QUNUSVZFEAMqpQMKEE14U3RhdHVzQ2F0ZWdvcnkSIgoeTVhfU1RBVFVTX0NB",
"VEVHT1JZX1VOU1BFQ0lGSUVEEAASHgoaTVhfU1RBVFVTX0NBVEVHT1JZX1VO",
"S05PV04QARIZChVNWF9TVEFUVVNfQ0FURUdPUllfT0sQAhIeChpNWF9TVEFU",
"VVNfQ0FURUdPUllfUEVORElORxADEh4KGk1YX1NUQVRVU19DQVRFR09SWV9X",
"QVJOSU5HEAQSKgomTVhfU1RBVFVTX0NBVEVHT1JZX0NPTU1VTklDQVRJT05f",
"RVJST1IQBRIqCiZNWF9TVEFUVVNfQ0FURUdPUllfQ09ORklHVVJBVElPTl9F",
"UlJPUhAGEigKJE1YX1NUQVRVU19DQVRFR09SWV9PUEVSQVRJT05BTF9FUlJP",
"UhAHEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TRUNVUklUWV9FUlJPUhAIEiUK",
"IU1YX1NUQVRVU19DQVRFR09SWV9TT0ZUV0FSRV9FUlJPUhAJEiIKHk1YX1NU",
"QVRVU19DQVRFR09SWV9PVEhFUl9FUlJPUhAKKsoCCg5NeFN0YXR1c1NvdXJj",
"ZRIgChxNWF9TVEFUVVNfU09VUkNFX1VOU1BFQ0lGSUVEEAASHAoYTVhfU1RB",
"VFVTX1NPVVJDRV9VTktOT1dOEAESIwofTVhfU1RBVFVTX1NPVVJDRV9SRVFV",
"RVNUSU5HX0xNWBACEiMKH01YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19M",
"TVgQAxIjCh9NWF9TVEFUVVNfU09VUkNFX1JFUVVFU1RJTkdfTk1YEAQSIwof",
"TVhfU1RBVFVTX1NPVVJDRV9SRVNQT05ESU5HX05NWBAFEjEKLU1YX1NUQVRV",
"U19TT1VSQ0VfUkVRVUVTVElOR19BVVRPTUFUSU9OX09CSkVDVBAGEjEKLU1Y",
"X1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19BVVRPTUFUSU9OX09CSkVDVBAH",
"Kt0ECgpNeERhdGFUeXBlEhwKGE1YX0RBVEFfVFlQRV9VTlNQRUNJRklFRBAA",
"EhgKFE1YX0RBVEFfVFlQRV9VTktOT1dOEAESGAoUTVhfREFUQV9UWVBFX05P",
"X0RBVEEQAhIYChRNWF9EQVRBX1RZUEVfQk9PTEVBThADEhgKFE1YX0RBVEFf",
"VFlQRV9JTlRFR0VSEAQSFgoSTVhfREFUQV9UWVBFX0ZMT0FUEAUSFwoTTVhf",
"REFUQV9UWVBFX0RPVUJMRRAGEhcKE01YX0RBVEFfVFlQRV9TVFJJTkcQBxIV",
"ChFNWF9EQVRBX1RZUEVfVElNRRAIEh0KGU1YX0RBVEFfVFlQRV9FTEFQU0VE",
"X1RJTUUQCRIfChtNWF9EQVRBX1RZUEVfUkVGRVJFTkNFX1RZUEUQChIcChhN",
"WF9EQVRBX1RZUEVfU1RBVFVTX1RZUEUQCxIVChFNWF9EQVRBX1RZUEVfRU5V",
"TRAMEi0KKU1YX0RBVEFfVFlQRV9TRUNVUklUWV9DTEFTU0lGSUNBVElPTl9F",
"TlVNEA0SIgoeTVhfREFUQV9UWVBFX0RBVEFfUVVBTElUWV9UWVBFEA4SHwob",
"TVhfREFUQV9UWVBFX1FVQUxJRklFRF9FTlVNEA8SIQodTVhfREFUQV9UWVBF",
"X1FVQUxJRklFRF9TVFJVQ1QQEBIpCiVNWF9EQVRBX1RZUEVfSU5URVJOQVRJ",
"T05BTElaRURfU1RSSU5HEBESGwoXTVhfREFUQV9UWVBFX0JJR19TVFJJTkcQ",
"EhIUChBNWF9EQVRBX1RZUEVfRU5EEBMqowMKElByb3RvY29sU3RhdHVzQ29k",
"ZRIkCiBQUk9UT0NPTF9TVEFUVVNfQ09ERV9VTlNQRUNJRklFRBAAEhsKF1BS",
"T1RPQ09MX1NUQVRVU19DT0RFX09LEAESKAokUFJPVE9DT0xfU1RBVFVTX0NP",
"REVfSU5WQUxJRF9SRVFVRVNUEAISKgomUFJPVE9DT0xfU1RBVFVTX0NPREVf",
"U0VTU0lPTl9OT1RfRk9VTkQQAxIqCiZQUk9UT0NPTF9TVEFUVVNfQ09ERV9T",
"RVNTSU9OX05PVF9SRUFEWRAEEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1dP",
"UktFUl9VTkFWQUlMQUJMRRAFEiAKHFBST1RPQ09MX1NUQVRVU19DT0RFX1RJ",
"TUVPVVQQBhIhCh1QUk9UT0NPTF9TVEFUVVNfQ09ERV9DQU5DRUxFRBAHEisK",
"J1BST1RPQ09MX1NUQVRVU19DT0RFX1BST1RPQ09MX1ZJT0xBVElPThAIEikK",
"JVBST1RPQ09MX1NUQVRVU19DT0RFX01YQUNDRVNTX0ZBSUxVUkUQCSq/AgoM",
"U2Vzc2lvblN0YXRlEh0KGVNFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIa",
"ChZTRVNTSU9OX1NUQVRFX0NSRUFUSU5HEAESIQodU0VTU0lPTl9TVEFURV9T",
"VEFSVElOR19XT1JLRVIQAhIiCh5TRVNTSU9OX1NUQVRFX1dBSVRJTkdfRk9S",
"X1BJUEUQAxIdChlTRVNTSU9OX1NUQVRFX0hBTkRTSEFLSU5HEAQSJQohU0VT",
"U0lPTl9TVEFURV9JTklUSUFMSVpJTkdfV09SS0VSEAUSFwoTU0VTU0lPTl9T",
"VEFURV9SRUFEWRAGEhkKFVNFU1NJT05fU1RBVEVfQ0xPU0lORxAHEhgKFFNF",
"U1NJT05fU1RBVEVfQ0xPU0VEEAgSGQoVU0VTU0lPTl9TVEFURV9GQVVMVEVE",
"EAkywwUKD014QWNjZXNzR2F0ZXdheRJdCgtPcGVuU2Vzc2lvbhInLm14YWNj",
"ZXNzX2dhdGV3YXkudjEuT3BlblNlc3Npb25SZXF1ZXN0GiUubXhhY2Nlc3Nf",
"Z2F0ZXdheS52MS5PcGVuU2Vzc2lvblJlcGx5EmAKDENsb3NlU2Vzc2lvbhIo",
"Lm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNzaW9uUmVxdWVzdBomLm14",
"YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNzaW9uUmVwbHkSVAoGSW52b2tl",
"EiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRSZXF1ZXN0GiMubXhh",
"Y2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRSZXBseRJYCgxTdHJlYW1FdmVu",
"dHMSKC5teGFjY2Vzc19nYXRld2F5LnYxLlN0cmVhbUV2ZW50c1JlcXVlc3Qa",
"HC5teGFjY2Vzc19nYXRld2F5LnYxLk14RXZlbnQwARJsChBBY2tub3dsZWRn",
"ZUFsYXJtEiwubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY2tub3dsZWRnZUFsYXJt",
"UmVxdWVzdBoqLm14YWNjZXNzX2dhdGV3YXkudjEuQWNrbm93bGVkZ2VBbGFy",
"bVJlcGx5EmEKDFN0cmVhbUFsYXJtcxIoLm14YWNjZXNzX2dhdGV3YXkudjEu",
"U3RyZWFtQWxhcm1zUmVxdWVzdBolLm14YWNjZXNzX2dhdGV3YXkudjEuQWxh",
"cm1GZWVkTWVzc2FnZTABEm4KEVF1ZXJ5QWN0aXZlQWxhcm1zEi0ubXhhY2Nl",
"c3NfZ2F0ZXdheS52MS5RdWVyeUFjdGl2ZUFsYXJtc1JlcXVlc3QaKC5teGFj",
"Y2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFsYXJtU25hcHNob3QwAUImqgIjWkIu",
"TU9NLldXLk14R2F0ZXdheS5Db250cmFjdHMuUHJvdG9iBnByb3RvMw=="));
"Y2Vzc19nYXRld2F5LnYxLkFsYXJtUHJvdmlkZXJTdGF0dXNIABJDCg9zbmFw",
"c2hvdF9zdGF0dXMYBSABKAsyKC5teGFjY2Vzc19nYXRld2F5LnYxLkFsYXJt",
"U25hcHNob3RTdGF0dXNIAEIJCgdwYXlsb2FkIpgBChNBbGFybVByb3ZpZGVy",
"U3RhdHVzEjQKBG1vZGUYASABKA4yJi5teGFjY2Vzc19nYXRld2F5LnYxLkFs",
"YXJtUHJvdmlkZXJNb2RlEhAKCGRlZ3JhZGVkGAIgASgIEg4KBnJlYXNvbhgD",
"IAEoCRIpCgVzaW5jZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3Rh",
"bXAiKAoTQWxhcm1TbmFwc2hvdFN0YXR1cxIRCgl0cnVuY2F0ZWQYASABKAgi",
"6wEKDU14U3RhdHVzUHJveHkSDwoHc3VjY2VzcxgBIAEoBRI3CghjYXRlZ29y",
"eRgCIAEoDjIlLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNDYXRlZ29y",
"eRI4CgtkZXRlY3RlZF9ieRgDIAEoDjIjLm14YWNjZXNzX2dhdGV3YXkudjEu",
"TXhTdGF0dXNTb3VyY2USDgoGZGV0YWlsGAQgASgFEhQKDHJhd19jYXRlZ29y",
"eRgFIAEoBRIXCg9yYXdfZGV0ZWN0ZWRfYnkYBiABKAUSFwoPZGlhZ25vc3Rp",
"Y190ZXh0GAcgASgJIukDCgdNeFZhbHVlEjIKCWRhdGFfdHlwZRgBIAEoDjIf",
"Lm14YWNjZXNzX2dhdGV3YXkudjEuTXhEYXRhVHlwZRIUCgx2YXJpYW50X3R5",
"cGUYAiABKAkSDwoHaXNfbnVsbBgDIAEoCBIWCg5yYXdfZGlhZ25vc3RpYxgE",
"IAEoCRIVCg1yYXdfZGF0YV90eXBlGAUgASgFEhQKCmJvb2xfdmFsdWUYCiAB",
"KAhIABIVCgtpbnQzMl92YWx1ZRgLIAEoBUgAEhUKC2ludDY0X3ZhbHVlGAwg",
"ASgDSAASFQoLZmxvYXRfdmFsdWUYDSABKAJIABIWCgxkb3VibGVfdmFsdWUY",
"DiABKAFIABIWCgxzdHJpbmdfdmFsdWUYDyABKAlIABI1Cg90aW1lc3RhbXBf",
"dmFsdWUYECABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAASMwoL",
"YXJyYXlfdmFsdWUYESABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14QXJy",
"YXlIABITCglyYXdfdmFsdWUYEiABKAxIABJAChJzcGFyc2VfYXJyYXlfdmFs",
"dWUYEyABKAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLk14U3BhcnNlQXJyYXlI",
"AEIGCgRraW5kIv4ECgdNeEFycmF5EjoKEWVsZW1lbnRfZGF0YV90eXBlGAEg",
"ASgOMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeERhdGFUeXBlEhQKDHZhcmlh",
"bnRfdHlwZRgCIAEoCRISCgpkaW1lbnNpb25zGAMgAygNEhYKDnJhd19kaWFn",
"bm9zdGljGAQgASgJEh0KFXJhd19lbGVtZW50X2RhdGFfdHlwZRgFIAEoBRI1",
"Cgtib29sX3ZhbHVlcxgKIAEoCzIeLm14YWNjZXNzX2dhdGV3YXkudjEuQm9v",
"bEFycmF5SAASNwoMaW50MzJfdmFsdWVzGAsgASgLMh8ubXhhY2Nlc3NfZ2F0",
"ZXdheS52MS5JbnQzMkFycmF5SAASNwoMaW50NjRfdmFsdWVzGAwgASgLMh8u",
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQ2NEFycmF5SAASNwoMZmxvYXRfdmFs",
"dWVzGA0gASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5GbG9hdEFycmF5SAAS",
"OQoNZG91YmxlX3ZhbHVlcxgOIAEoCzIgLm14YWNjZXNzX2dhdGV3YXkudjEu",
"RG91YmxlQXJyYXlIABI5Cg1zdHJpbmdfdmFsdWVzGA8gASgLMiAubXhhY2Nl",
"c3NfZ2F0ZXdheS52MS5TdHJpbmdBcnJheUgAEj8KEHRpbWVzdGFtcF92YWx1",
"ZXMYECABKAsyIy5teGFjY2Vzc19nYXRld2F5LnYxLlRpbWVzdGFtcEFycmF5",
"SAASMwoKcmF3X3ZhbHVlcxgRIAEoCzIdLm14YWNjZXNzX2dhdGV3YXkudjEu",
"UmF3QXJyYXlIAEIICgZ2YWx1ZXMimQEKDU14U3BhcnNlQXJyYXkSOgoRZWxl",
"bWVudF9kYXRhX3R5cGUYASABKA4yHy5teGFjY2Vzc19nYXRld2F5LnYxLk14",
"RGF0YVR5cGUSFAoMdG90YWxfbGVuZ3RoGAIgASgNEjYKCGVsZW1lbnRzGAMg",
"AygLMiQubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFNwYXJzZUVsZW1lbnQiTQoP",
"TXhTcGFyc2VFbGVtZW50Eg0KBWluZGV4GAEgASgNEisKBXZhbHVlGAIgASgL",
"MhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlIhsKCUJvb2xBcnJheRIO",
"CgZ2YWx1ZXMYASADKAgiHAoKSW50MzJBcnJheRIOCgZ2YWx1ZXMYASADKAUi",
"HAoKSW50NjRBcnJheRIOCgZ2YWx1ZXMYASADKAMiHAoKRmxvYXRBcnJheRIO",
"CgZ2YWx1ZXMYASADKAIiHQoLRG91YmxlQXJyYXkSDgoGdmFsdWVzGAEgAygB",
"Ih0KC1N0cmluZ0FycmF5Eg4KBnZhbHVlcxgBIAMoCSI8Cg5UaW1lc3RhbXBB",
"cnJheRIqCgZ2YWx1ZXMYASADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0",
"YW1wIhoKCFJhd0FycmF5Eg4KBnZhbHVlcxgBIAMoDCJYCg5Qcm90b2NvbFN0",
"YXR1cxI1CgRjb2RlGAEgASgOMicubXhhY2Nlc3NfZ2F0ZXdheS52MS5Qcm90",
"b2NvbFN0YXR1c0NvZGUSDwoHbWVzc2FnZRgCIAEoCSqfCwoNTXhDb21tYW5k",
"S2luZBIfChtNWF9DT01NQU5EX0tJTkRfVU5TUEVDSUZJRUQQABIcChhNWF9D",
"T01NQU5EX0tJTkRfUkVHSVNURVIQARIeChpNWF9DT01NQU5EX0tJTkRfVU5S",
"RUdJU1RFUhACEhwKGE1YX0NPTU1BTkRfS0lORF9BRERfSVRFTRADEh0KGU1Y",
"X0NPTU1BTkRfS0lORF9BRERfSVRFTTIQBBIfChtNWF9DT01NQU5EX0tJTkRf",
"UkVNT1ZFX0lURU0QBRIaChZNWF9DT01NQU5EX0tJTkRfQURWSVNFEAYSHQoZ",
"TVhfQ09NTUFORF9LSU5EX1VOX0FEVklTRRAHEiYKIk1YX0NPTU1BTkRfS0lO",
"RF9BRFZJU0VfU1VQRVJWSVNPUlkQCBIlCiFNWF9DT01NQU5EX0tJTkRfQURE",
"X0JVRkZFUkVEX0lURU0QCRIwCixNWF9DT01NQU5EX0tJTkRfU0VUX0JVRkZF",
"UkVEX1VQREFURV9JTlRFUlZBTBAKEhsKF01YX0NPTU1BTkRfS0lORF9TVVNQ",
"RU5EEAsSHAoYTVhfQ09NTUFORF9LSU5EX0FDVElWQVRFEAwSGQoVTVhfQ09N",
"TUFORF9LSU5EX1dSSVRFEA0SGgoWTVhfQ09NTUFORF9LSU5EX1dSSVRFMhAO",
"EiEKHU1YX0NPTU1BTkRfS0lORF9XUklURV9TRUNVUkVEEA8SIgoeTVhfQ09N",
"TUFORF9LSU5EX1dSSVRFX1NFQ1VSRUQyEBASJQohTVhfQ09NTUFORF9LSU5E",
"X0FVVEhFTlRJQ0FURV9VU0VSEBESKAokTVhfQ09NTUFORF9LSU5EX0FSQ0hF",
"U1RSQV9VU0VSX1RPX0lEEBISIQodTVhfQ09NTUFORF9LSU5EX0FERF9JVEVN",
"X0JVTEsQExIkCiBNWF9DT01NQU5EX0tJTkRfQURWSVNFX0lURU1fQlVMSxAU",
"EiQKIE1YX0NPTU1BTkRfS0lORF9SRU1PVkVfSVRFTV9CVUxLEBUSJwojTVhf",
"Q09NTUFORF9LSU5EX1VOX0FEVklTRV9JVEVNX0JVTEsQFhIiCh5NWF9DT01N",
"QU5EX0tJTkRfU1VCU0NSSUJFX0JVTEsQFxIkCiBNWF9DT01NQU5EX0tJTkRf",
"VU5TVUJTQ1JJQkVfQlVMSxAYEiQKIE1YX0NPTU1BTkRfS0lORF9TVUJTQ1JJ",
"QkVfQUxBUk1TEBkSJgoiTVhfQ09NTUFORF9LSU5EX1VOU1VCU0NSSUJFX0FM",
"QVJNUxAaEiUKIU1YX0NPTU1BTkRfS0lORF9BQ0tOT1dMRURHRV9BTEFSTRAb",
"EicKI01YX0NPTU1BTkRfS0lORF9RVUVSWV9BQ1RJVkVfQUxBUk1TEBwSLQop",
"TVhfQ09NTUFORF9LSU5EX0FDS05PV0xFREdFX0FMQVJNX0JZX05BTUUQHRIe",
"ChpNWF9DT01NQU5EX0tJTkRfV1JJVEVfQlVMSxAeEh8KG01YX0NPTU1BTkRf",
"S0lORF9XUklURTJfQlVMSxAfEiYKIk1YX0NPTU1BTkRfS0lORF9XUklURV9T",
"RUNVUkVEX0JVTEsQIBInCiNNWF9DT01NQU5EX0tJTkRfV1JJVEVfU0VDVVJF",
"RDJfQlVMSxAhEh0KGU1YX0NPTU1BTkRfS0lORF9SRUFEX0JVTEsQIhIYChRN",
"WF9DT01NQU5EX0tJTkRfUElORxBkEiUKIU1YX0NPTU1BTkRfS0lORF9HRVRf",
"U0VTU0lPTl9TVEFURRBlEiMKH01YX0NPTU1BTkRfS0lORF9HRVRfV09SS0VS",
"X0lORk8QZhIgChxNWF9DT01NQU5EX0tJTkRfRFJBSU5fRVZFTlRTEGcSIwof",
"TVhfQ09NTUFORF9LSU5EX1NIVVRET1dOX1dPUktFUhBoKnoKEUFsYXJtUHJv",
"dmlkZXJNb2RlEiMKH0FMQVJNX1BST1ZJREVSX01PREVfVU5TUEVDSUZJRUQQ",
"ABIgChxBTEFSTV9QUk9WSURFUl9NT0RFX0FMQVJNTUdSEAESHgoaQUxBUk1f",
"UFJPVklERVJfTU9ERV9TVUJUQUcQAiqtAgoNTXhFdmVudEZhbWlseRIfChtN",
"WF9FVkVOVF9GQU1JTFlfVU5TUEVDSUZJRUQQABIiCh5NWF9FVkVOVF9GQU1J",
"TFlfT05fREFUQV9DSEFOR0UQARIlCiFNWF9FVkVOVF9GQU1JTFlfT05fV1JJ",
"VEVfQ09NUExFVEUQAhImCiJNWF9FVkVOVF9GQU1JTFlfT1BFUkFUSU9OX0NP",
"TVBMRVRFEAMSKwonTVhfRVZFTlRfRkFNSUxZX09OX0JVRkZFUkVEX0RBVEFf",
"Q0hBTkdFEAQSJwojTVhfRVZFTlRfRkFNSUxZX09OX0FMQVJNX1RSQU5TSVRJ",
"T04QBRIyCi5NWF9FVkVOVF9GQU1JTFlfT05fQUxBUk1fUFJPVklERVJfTU9E",
"RV9DSEFOR0VEEAYqygEKE0FsYXJtVHJhbnNpdGlvbktpbmQSJQohQUxBUk1f",
"VFJBTlNJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQUxBUk1fVFJBTlNJ",
"VElPTl9LSU5EX1JBSVNFEAESJQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX0FD",
"S05PV0xFREdFEAISHwobQUxBUk1fVFJBTlNJVElPTl9LSU5EX0NMRUFSEAMS",
"IwofQUxBUk1fVFJBTlNJVElPTl9LSU5EX1JFVFJJR0dFUhAEKqoBChNBbGFy",
"bUNvbmRpdGlvblN0YXRlEiUKIUFMQVJNX0NPTkRJVElPTl9TVEFURV9VTlNQ",
"RUNJRklFRBAAEiAKHEFMQVJNX0NPTkRJVElPTl9TVEFURV9BQ1RJVkUQARIm",
"CiJBTEFSTV9DT05ESVRJT05fU1RBVEVfQUNUSVZFX0FDS0VEEAISIgoeQUxB",
"Uk1fQ09ORElUSU9OX1NUQVRFX0lOQUNUSVZFEAMqpQMKEE14U3RhdHVzQ2F0",
"ZWdvcnkSIgoeTVhfU1RBVFVTX0NBVEVHT1JZX1VOU1BFQ0lGSUVEEAASHgoa",
"TVhfU1RBVFVTX0NBVEVHT1JZX1VOS05PV04QARIZChVNWF9TVEFUVVNfQ0FU",
"RUdPUllfT0sQAhIeChpNWF9TVEFUVVNfQ0FURUdPUllfUEVORElORxADEh4K",
"Gk1YX1NUQVRVU19DQVRFR09SWV9XQVJOSU5HEAQSKgomTVhfU1RBVFVTX0NB",
"VEVHT1JZX0NPTU1VTklDQVRJT05fRVJST1IQBRIqCiZNWF9TVEFUVVNfQ0FU",
"RUdPUllfQ09ORklHVVJBVElPTl9FUlJPUhAGEigKJE1YX1NUQVRVU19DQVRF",
"R09SWV9PUEVSQVRJT05BTF9FUlJPUhAHEiUKIU1YX1NUQVRVU19DQVRFR09S",
"WV9TRUNVUklUWV9FUlJPUhAIEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TT0ZU",
"V0FSRV9FUlJPUhAJEiIKHk1YX1NUQVRVU19DQVRFR09SWV9PVEhFUl9FUlJP",
"UhAKKsoCCg5NeFN0YXR1c1NvdXJjZRIgChxNWF9TVEFUVVNfU09VUkNFX1VO",
"U1BFQ0lGSUVEEAASHAoYTVhfU1RBVFVTX1NPVVJDRV9VTktOT1dOEAESIwof",
"TVhfU1RBVFVTX1NPVVJDRV9SRVFVRVNUSU5HX0xNWBACEiMKH01YX1NUQVRV",
"U19TT1VSQ0VfUkVTUE9ORElOR19MTVgQAxIjCh9NWF9TVEFUVVNfU09VUkNF",
"X1JFUVVFU1RJTkdfTk1YEAQSIwofTVhfU1RBVFVTX1NPVVJDRV9SRVNQT05E",
"SU5HX05NWBAFEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVRVUVTVElOR19BVVRP",
"TUFUSU9OX09CSkVDVBAGEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElO",
"R19BVVRPTUFUSU9OX09CSkVDVBAHKt0ECgpNeERhdGFUeXBlEhwKGE1YX0RB",
"VEFfVFlQRV9VTlNQRUNJRklFRBAAEhgKFE1YX0RBVEFfVFlQRV9VTktOT1dO",
"EAESGAoUTVhfREFUQV9UWVBFX05PX0RBVEEQAhIYChRNWF9EQVRBX1RZUEVf",
"Qk9PTEVBThADEhgKFE1YX0RBVEFfVFlQRV9JTlRFR0VSEAQSFgoSTVhfREFU",
"QV9UWVBFX0ZMT0FUEAUSFwoTTVhfREFUQV9UWVBFX0RPVUJMRRAGEhcKE01Y",
"X0RBVEFfVFlQRV9TVFJJTkcQBxIVChFNWF9EQVRBX1RZUEVfVElNRRAIEh0K",
"GU1YX0RBVEFfVFlQRV9FTEFQU0VEX1RJTUUQCRIfChtNWF9EQVRBX1RZUEVf",
"UkVGRVJFTkNFX1RZUEUQChIcChhNWF9EQVRBX1RZUEVfU1RBVFVTX1RZUEUQ",
"CxIVChFNWF9EQVRBX1RZUEVfRU5VTRAMEi0KKU1YX0RBVEFfVFlQRV9TRUNV",
"UklUWV9DTEFTU0lGSUNBVElPTl9FTlVNEA0SIgoeTVhfREFUQV9UWVBFX0RB",
"VEFfUVVBTElUWV9UWVBFEA4SHwobTVhfREFUQV9UWVBFX1FVQUxJRklFRF9F",
"TlVNEA8SIQodTVhfREFUQV9UWVBFX1FVQUxJRklFRF9TVFJVQ1QQEBIpCiVN",
"WF9EQVRBX1RZUEVfSU5URVJOQVRJT05BTElaRURfU1RSSU5HEBESGwoXTVhf",
"REFUQV9UWVBFX0JJR19TVFJJTkcQEhIUChBNWF9EQVRBX1RZUEVfRU5EEBMq",
"owMKElByb3RvY29sU3RhdHVzQ29kZRIkCiBQUk9UT0NPTF9TVEFUVVNfQ09E",
"RV9VTlNQRUNJRklFRBAAEhsKF1BST1RPQ09MX1NUQVRVU19DT0RFX09LEAES",
"KAokUFJPVE9DT0xfU1RBVFVTX0NPREVfSU5WQUxJRF9SRVFVRVNUEAISKgom",
"UFJPVE9DT0xfU1RBVFVTX0NPREVfU0VTU0lPTl9OT1RfRk9VTkQQAxIqCiZQ",
"Uk9UT0NPTF9TVEFUVVNfQ09ERV9TRVNTSU9OX05PVF9SRUFEWRAEEisKJ1BS",
"T1RPQ09MX1NUQVRVU19DT0RFX1dPUktFUl9VTkFWQUlMQUJMRRAFEiAKHFBS",
"T1RPQ09MX1NUQVRVU19DT0RFX1RJTUVPVVQQBhIhCh1QUk9UT0NPTF9TVEFU",
"VVNfQ09ERV9DQU5DRUxFRBAHEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1BS",
"T1RPQ09MX1ZJT0xBVElPThAIEikKJVBST1RPQ09MX1NUQVRVU19DT0RFX01Y",
"QUNDRVNTX0ZBSUxVUkUQCSq/AgoMU2Vzc2lvblN0YXRlEh0KGVNFU1NJT05f",
"U1RBVEVfVU5TUEVDSUZJRUQQABIaChZTRVNTSU9OX1NUQVRFX0NSRUFUSU5H",
"EAESIQodU0VTU0lPTl9TVEFURV9TVEFSVElOR19XT1JLRVIQAhIiCh5TRVNT",
"SU9OX1NUQVRFX1dBSVRJTkdfRk9SX1BJUEUQAxIdChlTRVNTSU9OX1NUQVRF",
"X0hBTkRTSEFLSU5HEAQSJQohU0VTU0lPTl9TVEFURV9JTklUSUFMSVpJTkdf",
"V09SS0VSEAUSFwoTU0VTU0lPTl9TVEFURV9SRUFEWRAGEhkKFVNFU1NJT05f",
"U1RBVEVfQ0xPU0lORxAHEhgKFFNFU1NJT05fU1RBVEVfQ0xPU0VEEAgSGQoV",
"U0VTU0lPTl9TVEFURV9GQVVMVEVEEAkywwUKD014QWNjZXNzR2F0ZXdheRJd",
"CgtPcGVuU2Vzc2lvbhInLm14YWNjZXNzX2dhdGV3YXkudjEuT3BlblNlc3Np",
"b25SZXF1ZXN0GiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5PcGVuU2Vzc2lvblJl",
"cGx5EmAKDENsb3NlU2Vzc2lvbhIoLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xv",
"c2VTZXNzaW9uUmVxdWVzdBomLm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VT",
"ZXNzaW9uUmVwbHkSVAoGSW52b2tlEiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5N",
"eENvbW1hbmRSZXF1ZXN0GiMubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1h",
"bmRSZXBseRJYCgxTdHJlYW1FdmVudHMSKC5teGFjY2Vzc19nYXRld2F5LnYx",
"LlN0cmVhbUV2ZW50c1JlcXVlc3QaHC5teGFjY2Vzc19nYXRld2F5LnYxLk14",
"RXZlbnQwARJsChBBY2tub3dsZWRnZUFsYXJtEiwubXhhY2Nlc3NfZ2F0ZXdh",
"eS52MS5BY2tub3dsZWRnZUFsYXJtUmVxdWVzdBoqLm14YWNjZXNzX2dhdGV3",
"YXkudjEuQWNrbm93bGVkZ2VBbGFybVJlcGx5EmEKDFN0cmVhbUFsYXJtcxIo",
"Lm14YWNjZXNzX2dhdGV3YXkudjEuU3RyZWFtQWxhcm1zUmVxdWVzdBolLm14",
"YWNjZXNzX2dhdGV3YXkudjEuQWxhcm1GZWVkTWVzc2FnZTABEm4KEVF1ZXJ5",
"QWN0aXZlQWxhcm1zEi0ubXhhY2Nlc3NfZ2F0ZXdheS52MS5RdWVyeUFjdGl2",
"ZUFsYXJtc1JlcXVlc3QaKC5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2ZUFs",
"YXJtU25hcHNob3QwAUImqgIjWkIuTU9NLldXLk14R2F0ZXdheS5Db250cmFj",
"dHMuUHJvdG9iBnByb3RvMw=="));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, },
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxCommandKind), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderMode), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxEventFamily), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionKind), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmConditionState), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusCategory), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusSource), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxDataType), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.ProtocolStatusCode), typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.SessionState), }, null, new pbr::GeneratedClrTypeInfo[] {
@@ -616,8 +619,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmRequest.Parser, new[]{ "ClientCorrelationId", "AlarmFullReference", "Comment", "OperatorUser" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser, new[]{ "CorrelationId", "ProtocolStatus", "Hresult", "Status", "DiagnosticMessage" }, new[]{ "Hresult" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest), global::ZB.MOM.WW.MxGateway.Contracts.Proto.StreamAlarmsRequest.Parser, new[]{ "ClientCorrelationId", "AlarmFilterPrefix" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage.Parser, new[]{ "ActiveAlarm", "SnapshotComplete", "Transition", "ProviderStatus" }, new[]{ "Payload" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmFeedMessage.Parser, new[]{ "ActiveAlarm", "SnapshotComplete", "Transition", "ProviderStatus", "SnapshotStatus" }, new[]{ "Payload" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderStatus), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmProviderStatus.Parser, new[]{ "Mode", "Degraded", "Reason", "Since" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus), global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus.Parser, new[]{ "Truncated" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusProxy), global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusProxy.Parser, new[]{ "Success", "Category", "DetectedBy", "Detail", "RawCategory", "RawDetectedBy", "DiagnosticText" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxValue), global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxValue.Parser, new[]{ "DataType", "VariantType", "IsNull", "RawDiagnostic", "RawDataType", "BoolValue", "Int32Value", "Int64Value", "FloatValue", "DoubleValue", "StringValue", "TimestampValue", "ArrayValue", "RawValue", "SparseArrayValue" }, new[]{ "Kind" }, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxArray), global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxArray.Parser, new[]{ "ElementDataType", "VariantType", "Dimensions", "RawDiagnostic", "RawElementDataType", "BoolValues", "Int32Values", "Int64Values", "FloatValues", "DoubleValues", "StringValues", "TimestampValues", "RawValues" }, new[]{ "Values" }, null, null, null),
@@ -28561,6 +28565,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
case PayloadOneofCase.ProviderStatus:
ProviderStatus = other.ProviderStatus.Clone();
break;
case PayloadOneofCase.SnapshotStatus:
SnapshotStatus = other.SnapshotStatus.Clone();
break;
}
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
@@ -28648,6 +28655,23 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
}
/// <summary>Field number for the "snapshot_status" field.</summary>
public const int SnapshotStatusFieldNumber = 5;
/// <summary>
/// Snapshot-completeness status. Emitted once on stream open and again on
/// every change of the truncation verdict, so late joiners learn whether the
/// feed's active-alarm set may be incomplete.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus SnapshotStatus {
get { return payloadCase_ == PayloadOneofCase.SnapshotStatus ? (global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus) payload_ : null; }
set {
payload_ = value;
payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.SnapshotStatus;
}
}
private object payload_;
/// <summary>Enum of possible cases for the "payload" oneof.</summary>
public enum PayloadOneofCase {
@@ -28656,6 +28680,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
SnapshotComplete = 2,
Transition = 3,
ProviderStatus = 4,
SnapshotStatus = 5,
}
private PayloadOneofCase payloadCase_ = PayloadOneofCase.None;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -28690,6 +28715,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
if (SnapshotComplete != other.SnapshotComplete) return false;
if (!object.Equals(Transition, other.Transition)) return false;
if (!object.Equals(ProviderStatus, other.ProviderStatus)) return false;
if (!object.Equals(SnapshotStatus, other.SnapshotStatus)) return false;
if (PayloadCase != other.PayloadCase) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -28702,6 +28728,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
if (HasSnapshotComplete) hash ^= SnapshotComplete.GetHashCode();
if (payloadCase_ == PayloadOneofCase.Transition) hash ^= Transition.GetHashCode();
if (payloadCase_ == PayloadOneofCase.ProviderStatus) hash ^= ProviderStatus.GetHashCode();
if (payloadCase_ == PayloadOneofCase.SnapshotStatus) hash ^= SnapshotStatus.GetHashCode();
hash ^= (int) payloadCase_;
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
@@ -28737,6 +28764,10 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
output.WriteRawTag(34);
output.WriteMessage(ProviderStatus);
}
if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
output.WriteRawTag(42);
output.WriteMessage(SnapshotStatus);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
@@ -28763,6 +28794,10 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
output.WriteRawTag(34);
output.WriteMessage(ProviderStatus);
}
if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
output.WriteRawTag(42);
output.WriteMessage(SnapshotStatus);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
@@ -28785,6 +28820,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
if (payloadCase_ == PayloadOneofCase.ProviderStatus) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(ProviderStatus);
}
if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
size += 1 + pb::CodedOutputStream.ComputeMessageSize(SnapshotStatus);
}
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
@@ -28819,6 +28857,12 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
ProviderStatus.MergeFrom(other.ProviderStatus);
break;
case PayloadOneofCase.SnapshotStatus:
if (SnapshotStatus == null) {
SnapshotStatus = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus();
}
SnapshotStatus.MergeFrom(other.SnapshotStatus);
break;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
@@ -28871,6 +28915,15 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
ProviderStatus = subBuilder;
break;
}
case 42: {
global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus();
if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
subBuilder.MergeFrom(SnapshotStatus);
}
input.ReadMessage(subBuilder);
SnapshotStatus = subBuilder;
break;
}
}
}
#endif
@@ -28921,6 +28974,15 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
ProviderStatus = subBuilder;
break;
}
case 42: {
global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus subBuilder = new global::ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmSnapshotStatus();
if (payloadCase_ == PayloadOneofCase.SnapshotStatus) {
subBuilder.MergeFrom(SnapshotStatus);
}
input.ReadMessage(subBuilder);
SnapshotStatus = subBuilder;
break;
}
}
}
}
@@ -29252,6 +29314,218 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
/// <summary>
/// Feed-level snapshot-completeness status. Emitted once on StreamAlarms open
/// (after the initial provider_status frame, before the cached active_alarm
/// frames) so late joiners learn the current verdict, and again on every change
/// of the truncation verdict observed at reconcile. Mirrors the per-record
/// ActiveAlarmSnapshot.from_truncated_snapshot caveat at feed level so live
/// consumers can reason about completeness without polling QueryActiveAlarms.
/// </summary>
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class AlarmSnapshotStatus : pb::IMessage<AlarmSnapshotStatus>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
, pb::IBufferMessage
#endif
{
private static readonly pb::MessageParser<AlarmSnapshotStatus> _parser = new pb::MessageParser<AlarmSnapshotStatus>(() => new AlarmSnapshotStatus());
private pb::UnknownFieldSet _unknownFields;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pb::MessageParser<AlarmSnapshotStatus> Parser { get { return _parser; } }
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[87]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
pbr::MessageDescriptor pb::IMessage.Descriptor {
get { return Descriptor; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public AlarmSnapshotStatus() {
OnConstruction();
}
partial void OnConstruction();
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public AlarmSnapshotStatus(AlarmSnapshotStatus other) : this() {
truncated_ = other.truncated_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public AlarmSnapshotStatus Clone() {
return new AlarmSnapshotStatus(this);
}
/// <summary>Field number for the "truncated" field.</summary>
public const int TruncatedFieldNumber = 1;
private bool truncated_;
/// <summary>
/// True while the monitor's cached active-alarm set derives from a truncated
/// (capped) worker fetch — the set may be missing alarms. Distinct from
/// provider degradation (AlarmProviderStatus.degraded), which describes the
/// fidelity of the records rather than the completeness of the set.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool Truncated {
get { return truncated_; }
set {
truncated_ = value;
}
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override bool Equals(object other) {
return Equals(other as AlarmSnapshotStatus);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public bool Equals(AlarmSnapshotStatus other) {
if (ReferenceEquals(other, null)) {
return false;
}
if (ReferenceEquals(other, this)) {
return true;
}
if (Truncated != other.Truncated) return false;
return Equals(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override int GetHashCode() {
int hash = 1;
if (Truncated != false) hash ^= Truncated.GetHashCode();
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
return hash;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public override string ToString() {
return pb::JsonFormatter.ToDiagnosticString(this);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void WriteTo(pb::CodedOutputStream output) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
output.WriteRawMessage(this);
#else
if (Truncated != false) {
output.WriteRawTag(8);
output.WriteBool(Truncated);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
if (Truncated != false) {
output.WriteRawTag(8);
output.WriteBool(Truncated);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
}
}
#endif
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public int CalculateSize() {
int size = 0;
if (Truncated != false) {
size += 1 + 1;
}
if (_unknownFields != null) {
size += _unknownFields.CalculateSize();
}
return size;
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(AlarmSnapshotStatus other) {
if (other == null) {
return;
}
if (other.Truncated != false) {
Truncated = other.Truncated;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public void MergeFrom(pb::CodedInputStream input) {
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
input.ReadRawMessage(this);
#else
uint tag;
while ((tag = input.ReadTag()) != 0) {
if ((tag & 7) == 4) {
// Abort on any end group tag.
return;
}
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
break;
case 8: {
Truncated = input.ReadBool();
break;
}
}
}
#endif
}
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
uint tag;
while ((tag = input.ReadTag()) != 0) {
if ((tag & 7) == 4) {
// Abort on any end group tag.
return;
}
switch(tag) {
default:
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
break;
case 8: {
Truncated = input.ReadBool();
break;
}
}
}
}
#endif
}
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class MxStatusProxy : pb::IMessage<MxStatusProxy>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
@@ -29267,7 +29541,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[87]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[88]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -29698,7 +29972,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[88]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[89]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -30608,7 +30882,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[89]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[90]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -31402,7 +31676,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[90]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[91]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -31663,7 +31937,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[91]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[92]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -31910,7 +32184,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[92]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[93]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32099,7 +32373,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[93]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[94]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32288,7 +32562,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[94]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[95]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32477,7 +32751,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[95]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[96]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32666,7 +32940,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[96]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[97]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -32855,7 +33129,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[97]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[98]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -33042,7 +33316,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[98]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[99]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -33229,7 +33503,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[99]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[100]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -33416,7 +33690,7 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pbr::MessageDescriptor Descriptor {
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[100]; }
get { return global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[101]; }
}
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
@@ -1018,6 +1018,10 @@ message AlarmFeedMessage {
// Provider-mode status. Emitted once on stream open and again on every
// failover/failback so late joiners learn the current mode immediately.
AlarmProviderStatus provider_status = 4;
// Snapshot-completeness status. Emitted once on stream open and again on
// every change of the truncation verdict, so late joiners learn whether the
// feed's active-alarm set may be incomplete.
AlarmSnapshotStatus snapshot_status = 5;
}
}
@@ -1028,6 +1032,20 @@ message AlarmProviderStatus {
google.protobuf.Timestamp since = 4;
}
// Feed-level snapshot-completeness status. Emitted once on StreamAlarms open
// (after the initial provider_status frame, before the cached active_alarm
// frames) so late joiners learn the current verdict, and again on every change
// of the truncation verdict observed at reconcile. Mirrors the per-record
// ActiveAlarmSnapshot.from_truncated_snapshot caveat at feed level so live
// consumers can reason about completeness without polling QueryActiveAlarms.
message AlarmSnapshotStatus {
// True while the monitor's cached active-alarm set derives from a truncated
// (capped) worker fetch — the set may be missing alarms. Distinct from
// provider degradation (AlarmProviderStatus.degraded), which describes the
// fidelity of the records rather than the completeness of the set.
bool truncated = 1;
}
message MxStatusProxy {
// Mirrors the `success` member of the MXAccess MXSTATUS_PROXY struct
// (a 16-bit signed value in the COM struct, widened to int32 on the
@@ -624,11 +624,13 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
// at-least-once: consumers must still treat alarm state idempotently — apply a transition as
// "set the alarm to this state", never as an increment or a toggle.
//
// Truncation (`snapshotTruncated`) needs no special handling here, and that is worth saying
// Truncation (`snapshotTruncated`) needs no per-alarm handling here, and that is worth saying
// because the obvious worry — a capped fetch reading as a wave of Clears — is answered one
// level down. The worker merges rather than replaces its retained snapshot on a capped fetch,
// so the set arriving here still carries the alarms the capped reply had no room to mention.
// The flag is therefore only recorded, for the operator-facing completeness caveat.
// The flag is set-level status, not a delta: it is recorded, and a CHANGE of verdict is pushed
// to the feed as a snapshot_status frame (SetSnapshotTruncated) so live consumers learn the
// completeness caveat without polling QueryActiveAlarms.
private void ApplyReconcile(IEnumerable<ActiveAlarmSnapshot> snapshots, bool snapshotTruncated)
{
Dictionary<string, ActiveAlarmSnapshot> next = new(StringComparer.Ordinal);
@@ -688,11 +690,30 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
_alarms[incoming.Key] = incoming.Value;
}
_snapshotTruncated = snapshotTruncated;
SetSnapshotTruncated(snapshotTruncated);
_currentAlarmsProjection = null;
}
}
// Caller holds _sync. Records the truncation verdict and, on a CHANGE of verdict, pushes the
// feed-level snapshot_status frame. Edge-triggered rather than per-reconcile: a status frame
// repeated every reconcile interval is one consumers learn to ignore. The verdict describes the
// whole cached set, not one alarm, so — like provider status — it goes to every subscriber
// regardless of alarm-filter prefix.
private void SetSnapshotTruncated(bool truncated)
{
if (_snapshotTruncated == truncated)
{
return;
}
_snapshotTruncated = truncated;
BroadcastToAll(new AlarmFeedMessage
{
SnapshotStatus = new AlarmSnapshotStatus { Truncated = truncated },
});
}
// Caller holds _sync. Pushes a feed message to every matching subscriber;
// a subscriber that has fallen behind is completed with an error and dropped.
private void Broadcast(AlarmFeedMessage message, string reference)
@@ -738,8 +759,10 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
_alarms.Clear();
// The truncation verdict describes the cache generation being discarded, so it goes
// with it. Carrying it across a monitor restart would caveat an empty set as "may be
// incomplete" on evidence from a session that no longer exists.
_snapshotTruncated = false;
// incomplete" on evidence from a session that no longer exists. Dropping a truncated
// verdict IS a completeness change, and feed subscribers outlive the monitor's worker
// session, so this routes through the edge path and they see the clearing frame.
SetSnapshotTruncated(false);
_currentAlarmsProjection = null;
}
}
@@ -761,13 +784,16 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
ActiveAlarmSnapshot[] snapshot;
AlarmProviderStatus providerStatus;
bool snapshotTruncated;
lock (_sync)
{
// Register before snapshotting under the same lock so neither a
// transition nor a provider-mode change can slip between the snapshot
// and the live stream.
// and the live stream. The truncation verdict is read here too, so the
// caveat and the set it qualifies are a consistent pair.
_subscribers.Add(subscriber);
providerStatus = BuildProviderStatus();
snapshotTruncated = _snapshotTruncated;
snapshot = _alarms.Values
.Where(alarm => prefix.Length == 0
|| alarm.AlarmFullReference.StartsWith(prefix, StringComparison.Ordinal))
@@ -781,6 +807,15 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
// learns the mode (and whether the feed is degraded) before any alarms.
yield return new AlarmFeedMessage { ProviderStatus = providerStatus };
// Then the completeness caveat, BEFORE the cached snapshot it qualifies: a consumer
// applying the snapshot as it streams needs to know whether the set may be missing
// alarms while it applies it, not after. Unconditional — an explicit false is what
// separates "the set is complete" from "this gateway never sends the frame".
yield return new AlarmFeedMessage
{
SnapshotStatus = new AlarmSnapshotStatus { Truncated = snapshotTruncated },
};
foreach (ActiveAlarmSnapshot alarm in snapshot)
{
yield return new AlarmFeedMessage { ActiveAlarm = alarm };
@@ -55,14 +55,25 @@ public interface IGatewayAlarmService
/// the intended granularity for a completeness hint; pairing them exactly
/// would need a combined accessor this seam deliberately does not have.
/// </para>
/// <para>
/// This is the polled read of the verdict. The same verdict is pushed to
/// the live feed as the <c>snapshot_status</c>
/// (<c>AlarmSnapshotStatus</c>) case of <see cref="AlarmFeedMessage"/> —
/// once at <see cref="StreamAsync"/> open and again on every change — so
/// a streaming consumer need not poll this property.
/// </para>
/// </summary>
bool SnapshotTruncated { get; }
/// <summary>
/// Attaches to the central alarm feed. The returned stream yields one
/// <see cref="AlarmFeedMessage"/> per currently-active alarm, then a
/// single <c>snapshot_complete</c> sentinel, then a <c>transition</c>
/// for every subsequent change.
/// Attaches to the central alarm feed. The returned stream opens with a
/// <c>provider_status</c> and a <c>snapshot_status</c>
/// <see cref="AlarmFeedMessage"/> — the current provider mode and
/// completeness verdict, so the caveats precede the records they
/// qualify — then one message per currently-active alarm, then a single
/// <c>snapshot_complete</c> sentinel, then a <c>transition</c> for every
/// subsequent change (and a further status message on every provider-mode
/// or truncation-verdict change).
/// </summary>
/// <param name="alarmFilterPrefix">Optional alarm-reference prefix scoping the feed.</param>
/// <param name="cancellationToken">Token that ends the subscription.</param>
@@ -16,7 +16,8 @@ namespace ZB.MOM.WW.MxGateway.Tests.Alarms;
/// <summary>
/// Carries the worker's truncated-fetch verdict across the gateway: worker
/// reply payload → <see cref="GatewayAlarmMonitor"/> → the public
/// <c>QueryActiveAlarms</c> stream.
/// <c>QueryActiveAlarms</c> stream (per record) and the live
/// <c>StreamAlarms</c> feed (the <c>snapshot_status</c> frame).
/// </summary>
/// <remarks>
/// <para>
@@ -145,6 +146,302 @@ public sealed class AlarmTruncationSignalTests
await monitor.StopAsync(CancellationToken.None);
}
/// <summary>
/// The feed-level edge. A reconcile that flips the verdict to truncated
/// pushes exactly one <c>snapshot_status</c> frame to every attached
/// subscriber; a following reconcile carrying the same verdict pushes
/// none. Without the edge test the frame could be emitted per reconcile —
/// a status frame every few seconds forever, which consumers would learn
/// to ignore.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReconcileFlippingToTruncated_EmitsOneSnapshotStatusFrame()
{
using GatewayMetrics metrics = new();
StubSessionManager sessions = new();
using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
using CancellationTokenSource cts = new();
await monitor.StartAsync(cts.Token);
await sessions.WaitForReconcileAsync(WaitTimeout);
List<AlarmFeedMessage> received = [];
TaskCompletionSource attached = new(TaskCreationOptions.RunContinuationsAsynchronously);
using CancellationTokenSource streamCts = new();
Task reader = ReadFeedAsync(monitor, received, attached, streamCts.Token);
await attached.Task.WaitAsync(WaitTimeout);
// The worker's next fetch comes back capped. A provider-mode event forces the reconcile
// immediately, so the test never waits on the periodic timer; each probe also broadcasts a
// provider_status frame, which is the barrier the assertions below count on.
sessions.SnapshotTruncated = true;
sessions.EmitEvent(ProviderModeProbe(1));
// A second, identical reconcile: same verdict, so no second frame. The third probe is only
// a barrier — the monitor handles provider-mode events one at a time and awaits the
// reconcile inside each, so its provider_status proves probe 2's reconcile has completed.
sessions.EmitEvent(ProviderModeProbe(2));
sessions.EmitEvent(ProviderModeProbe(3));
await WaitUntilAsync(() => CountOf(received, AlarmFeedMessage.PayloadOneofCase.ProviderStatus) >= 4, WaitTimeout);
lock (received)
{
bool[] verdicts = received
.Where(m => m.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotStatus)
.Select(m => m.SnapshotStatus.Truncated)
.ToArray();
// The first is this subscriber's open-time baseline; the second is the edge.
Assert.Equal([false, true], verdicts);
}
await streamCts.CancelAsync();
await reader;
await cts.CancelAsync();
await monitor.StopAsync(CancellationToken.None);
}
/// <summary>
/// The clearing edge. A fetch that comes back under the cap restores
/// absence authority, and the feed says so — otherwise a consumer that
/// saw the truncated frame would caveat its alarm set forever.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReconcileClearingTruncation_EmitsTheClearingFrame()
{
using GatewayMetrics metrics = new();
StubSessionManager sessions = new() { SnapshotTruncated = true };
using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
using CancellationTokenSource cts = new();
await monitor.StartAsync(cts.Token);
await sessions.WaitForReconcileAsync(WaitTimeout);
await WaitUntilAsync(() => monitor.SnapshotTruncated, WaitTimeout);
List<AlarmFeedMessage> received = [];
TaskCompletionSource attached = new(TaskCreationOptions.RunContinuationsAsynchronously);
using CancellationTokenSource streamCts = new();
Task reader = ReadFeedAsync(monitor, received, attached, streamCts.Token);
await attached.Task.WaitAsync(WaitTimeout);
sessions.SnapshotTruncated = false;
sessions.EmitEvent(ProviderModeProbe(1));
await WaitUntilAsync(
() => CountOf(received, AlarmFeedMessage.PayloadOneofCase.SnapshotStatus) >= 2,
WaitTimeout);
lock (received)
{
bool[] verdicts = received
.Where(m => m.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotStatus)
.Select(m => m.SnapshotStatus.Truncated)
.ToArray();
Assert.Equal([true, false], verdicts);
}
await streamCts.CancelAsync();
await reader;
await cts.CancelAsync();
await monitor.StopAsync(CancellationToken.None);
}
/// <summary>
/// Late-joiner priming while the verdict is truncated. The frame must
/// arrive after <c>provider_status</c> and before the cached
/// <c>active_alarm</c> frames: a consumer that applies the snapshot as it
/// streams needs the completeness caveat in hand before the records it
/// qualifies, not after.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscriberAttachingWhileTruncated_IsPrimedBeforeTheCachedSnapshot()
{
using GatewayMetrics metrics = new();
StubSessionManager sessions = new()
{
SnapshotTruncated = true,
Snapshots = [NewSnapshot("Galaxy!Area.Tank01.Level.HiHi", fromTruncatedSnapshot: true)],
};
using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
using CancellationTokenSource cts = new();
await monitor.StartAsync(cts.Token);
await sessions.WaitForReconcileAsync(WaitTimeout);
await WaitUntilAsync(() => monitor.SnapshotTruncated && monitor.CurrentAlarms.Count == 1, WaitTimeout);
AlarmFeedMessage[] preamble = await ReadPreambleAsync(monitor);
Assert.Equal(
[
AlarmFeedMessage.PayloadOneofCase.ProviderStatus,
AlarmFeedMessage.PayloadOneofCase.SnapshotStatus,
AlarmFeedMessage.PayloadOneofCase.ActiveAlarm,
AlarmFeedMessage.PayloadOneofCase.SnapshotComplete,
],
preamble.Select(m => m.PayloadCase).ToArray());
Assert.True(preamble[1].SnapshotStatus.Truncated);
await cts.CancelAsync();
await monitor.StopAsync(CancellationToken.None);
}
/// <summary>
/// The control for the priming frame: it is unconditional, so a
/// subscriber attaching to a complete feed is told so explicitly rather
/// than having to infer "no frame means not truncated" — an inference
/// that is indistinguishable from a gateway that never sends the frame.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscriberAttachingWhileComplete_IsPrimedWithTheFalseVerdict()
{
using GatewayMetrics metrics = new();
StubSessionManager sessions = new();
using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
using CancellationTokenSource cts = new();
await monitor.StartAsync(cts.Token);
await sessions.WaitForReconcileAsync(WaitTimeout);
AlarmFeedMessage[] preamble = await ReadPreambleAsync(monitor);
Assert.Equal(
[
AlarmFeedMessage.PayloadOneofCase.ProviderStatus,
AlarmFeedMessage.PayloadOneofCase.SnapshotStatus,
AlarmFeedMessage.PayloadOneofCase.SnapshotComplete,
],
preamble.Select(m => m.PayloadCase).ToArray());
Assert.False(preamble[1].SnapshotStatus.Truncated);
await cts.CancelAsync();
await monitor.StopAsync(CancellationToken.None);
}
/// <summary>
/// A monitor restart drops the truncation verdict with the cache
/// generation it describes. That drop is a completeness change like any
/// other, so subscribers — which outlive the monitor's worker session —
/// must see the clearing frame; otherwise a feed that silently re-seeds
/// leaves them caveating a set that is no longer truncated.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task MonitorRestartClearingTruncation_EmitsTheClearingFrame()
{
using GatewayMetrics metrics = new();
StubSessionManager sessions = new() { SnapshotTruncated = true };
using GatewayAlarmMonitor monitor = CreateMonitor(sessions, metrics);
using CancellationTokenSource cts = new();
await monitor.StartAsync(cts.Token);
await sessions.WaitForReconcileAsync(WaitTimeout);
await WaitUntilAsync(() => monitor.SnapshotTruncated, WaitTimeout);
List<AlarmFeedMessage> received = [];
TaskCompletionSource attached = new(TaskCreationOptions.RunContinuationsAsynchronously);
using CancellationTokenSource streamCts = new();
Task reader = ReadFeedAsync(monitor, received, attached, streamCts.Token);
await attached.Task.WaitAsync(WaitTimeout);
// Ending the worker event stream faults the monitor lifecycle, which clears the cache.
sessions.EndWorkerEventStream();
await WaitUntilAsync(
() => CountOf(received, AlarmFeedMessage.PayloadOneofCase.SnapshotStatus) >= 2,
WaitTimeout);
lock (received)
{
// Only the first two: the supervisor re-opens the session after its backoff, and that
// second lifecycle legitimately re-reports the truncated verdict.
bool[] verdicts = received
.Where(m => m.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotStatus)
.Select(m => m.SnapshotStatus.Truncated)
.Take(2)
.ToArray();
Assert.Equal([true, false], verdicts);
}
await streamCts.CancelAsync();
await reader;
await cts.CancelAsync();
await monitor.StopAsync(CancellationToken.None);
}
private static int CountOf(List<AlarmFeedMessage> received, AlarmFeedMessage.PayloadOneofCase payloadCase)
{
lock (received)
{
return received.Count(m => m.PayloadCase == payloadCase);
}
}
// Drains the monitor's feed into received (every frame, preamble included), signalling gate
// once the subscriber is registered — that is, on its first frame.
private static Task ReadFeedAsync(
GatewayAlarmMonitor monitor,
List<AlarmFeedMessage> received,
TaskCompletionSource gate,
CancellationToken cancellationToken)
{
return Task.Run(
async () =>
{
try
{
await foreach (AlarmFeedMessage message in monitor.StreamAsync(null, cancellationToken))
{
lock (received)
{
received.Add(message);
}
gate.TrySetResult();
}
}
catch (OperationCanceledException)
{
// Expected when the test cancels the stream.
}
},
CancellationToken.None);
}
// Reads one subscriber's open-time preamble: everything up to and including snapshot_complete.
private static async Task<AlarmFeedMessage[]> ReadPreambleAsync(GatewayAlarmMonitor monitor)
{
List<AlarmFeedMessage> preamble = [];
using CancellationTokenSource streamCts = new(WaitTimeout);
await foreach (AlarmFeedMessage message in monitor.StreamAsync(null, streamCts.Token))
{
preamble.Add(message);
if (message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotComplete)
{
break;
}
}
return [.. preamble];
}
// A no-op provider-mode event. The monitor forces an immediate reconcile after every one,
// which is how these tests drive a reconcile pass without waiting on the periodic timer.
private static MxEvent ProviderModeProbe(ulong sequence) => new()
{
Family = MxEventFamily.OnAlarmProviderModeChanged,
WorkerSequence = sequence,
OnAlarmProviderModeChanged = new OnAlarmProviderModeChangedEvent
{
Mode = AlarmProviderMode.Alarmmgr,
Reason = "probe",
At = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow),
},
};
private static ActiveAlarmSnapshot NewSnapshot(string reference, bool fromTruncatedSnapshot)
{
return new ActiveAlarmSnapshot
@@ -226,18 +523,43 @@ public sealed class AlarmTruncationSignalTests
private readonly Channel<WorkerEvent> _events = Channel.CreateUnbounded<WorkerEvent>();
private readonly TaskCompletionSource _reconciled =
new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly object _sync = new();
private bool _snapshotTruncated;
private IReadOnlyList<ActiveAlarmSnapshot> _snapshots = [];
/// <summary>Gets or sets the truncation verdict the scripted reply carries.</summary>
public bool SnapshotTruncated { get; init; }
/// <summary>
/// Gets or sets the truncation verdict the scripted reply carries. Settable mid-test so
/// a suite can drive the verdict across reconciles and observe the feed-level edge.
/// </summary>
public bool SnapshotTruncated
{
get { lock (_sync) { return _snapshotTruncated; } }
set { lock (_sync) { _snapshotTruncated = value; } }
}
/// <summary>Gets or sets the snapshots the scripted reply carries.</summary>
public IReadOnlyList<ActiveAlarmSnapshot> Snapshots { get; init; } = [];
public IReadOnlyList<ActiveAlarmSnapshot> Snapshots
{
get { lock (_sync) { return _snapshots; } }
set { lock (_sync) { _snapshots = value; } }
}
/// <summary>Completes once the monitor has issued its first QueryActiveAlarms.</summary>
/// <param name="timeout">The maximum time to wait.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task WaitForReconcileAsync(TimeSpan timeout) => _reconciled.Task.WaitAsync(timeout);
/// <summary>Pushes a worker event into the session's distributor pump.</summary>
/// <param name="mxEvent">The event to push.</param>
public void EmitEvent(MxEvent mxEvent) =>
_events.Writer.TryWrite(new WorkerEvent { Event = mxEvent });
/// <summary>
/// Ends the worker event stream, which faults the monitor's lifecycle exactly as a
/// worker exit would and takes it through its cache-clearing teardown.
/// </summary>
public void EndWorkerEventStream() => _events.Writer.TryComplete();
/// <inheritdoc />
public Task<GatewaySession> OpenSessionAsync(
SessionOpenRequest request,
@@ -275,8 +597,13 @@ public sealed class AlarmTruncationSignalTests
if (command.Command?.Kind == MxCommandKind.QueryActiveAlarms)
{
QueryActiveAlarmsReplyPayload payload = new() { SnapshotTruncated = SnapshotTruncated };
payload.Snapshots.AddRange(Snapshots.Select(snapshot => snapshot.Clone()));
QueryActiveAlarmsReplyPayload payload = new();
lock (_sync)
{
payload.SnapshotTruncated = _snapshotTruncated;
payload.Snapshots.AddRange(_snapshots.Select(snapshot => snapshot.Clone()));
}
reply.QueryActiveAlarms = payload;
_reconciled.TrySetResult();
}