diff --git a/docs/DesignDecisions.md b/docs/DesignDecisions.md index f3fd10b..32055df 100644 --- a/docs/DesignDecisions.md +++ b/docs/DesignDecisions.md @@ -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. diff --git a/docs/Grpc.md b/docs/Grpc.md index 271434c..15bd1c8 100644 --- a/docs/Grpc.md +++ b/docs/Grpc.md @@ -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 | diff --git a/gateway.md b/gateway.md index 1013b24..610904a 100644 --- a/gateway.md +++ b/gateway.md @@ -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 diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs index feadce8..2979802 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Generated/MxaccessGateway.cs @@ -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 { } } + /// Field number for the "snapshot_status" field. + public const int SnapshotStatusFieldNumber = 5; + /// + /// 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. + /// + [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_; /// Enum of possible cases for the "payload" oneof. 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 { } + /// + /// 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. + /// + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class AlarmSnapshotStatus : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new AlarmSnapshotStatus()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser 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); + } + + /// Field number for the "truncated" field. + public const int TruncatedFieldNumber = 1; + private bool truncated_; + /// + /// 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. + /// + [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 #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] 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 0ee3fb2..85d13a7 100644 --- a/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto +++ b/src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto @@ -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 diff --git a/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs b/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs index 77aa58c..9058615 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs @@ -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 snapshots, bool snapshotTruncated) { Dictionary 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 }; diff --git a/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs b/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs index 413fbcb..54bcb00 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs @@ -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. /// + /// + /// This is the polled read of the verdict. The same verdict is pushed to + /// the live feed as the snapshot_status + /// (AlarmSnapshotStatus) case of — + /// once at open and again on every change — so + /// a streaming consumer need not poll this property. + /// /// bool SnapshotTruncated { get; } /// - /// Attaches to the central alarm feed. The returned stream yields one - /// per currently-active alarm, then a - /// single snapshot_complete sentinel, then a transition - /// for every subsequent change. + /// Attaches to the central alarm feed. The returned stream opens with a + /// provider_status and a snapshot_status + /// — 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 + /// snapshot_complete sentinel, then a transition for every + /// subsequent change (and a further status message on every provider-mode + /// or truncation-verdict change). /// /// Optional alarm-reference prefix scoping the feed. /// Token that ends the subscription. diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmTruncationSignalTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmTruncationSignalTests.cs index e8f8772..b226a22 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmTruncationSignalTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmTruncationSignalTests.cs @@ -16,7 +16,8 @@ namespace ZB.MOM.WW.MxGateway.Tests.Alarms; /// /// Carries the worker's truncated-fetch verdict across the gateway: worker /// reply payload → → the public -/// QueryActiveAlarms stream. +/// QueryActiveAlarms stream (per record) and the live +/// StreamAlarms feed (the snapshot_status frame). /// /// /// @@ -145,6 +146,302 @@ public sealed class AlarmTruncationSignalTests await monitor.StopAsync(CancellationToken.None); } + /// + /// The feed-level edge. A reconcile that flips the verdict to truncated + /// pushes exactly one snapshot_status 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. + /// + /// A task that represents the asynchronous operation. + [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 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); + } + + /// + /// 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. + /// + /// A task that represents the asynchronous operation. + [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 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); + } + + /// + /// Late-joiner priming while the verdict is truncated. The frame must + /// arrive after provider_status and before the cached + /// active_alarm frames: a consumer that applies the snapshot as it + /// streams needs the completeness caveat in hand before the records it + /// qualifies, not after. + /// + /// A task that represents the asynchronous operation. + [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); + } + + /// + /// 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. + /// + /// A task that represents the asynchronous operation. + [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); + } + + /// + /// 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. + /// + /// A task that represents the asynchronous operation. + [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 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 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 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 ReadPreambleAsync(GatewayAlarmMonitor monitor) + { + List 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 _events = Channel.CreateUnbounded(); private readonly TaskCompletionSource _reconciled = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly object _sync = new(); + private bool _snapshotTruncated; + private IReadOnlyList _snapshots = []; - /// Gets or sets the truncation verdict the scripted reply carries. - public bool SnapshotTruncated { get; init; } + /// + /// 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. + /// + public bool SnapshotTruncated + { + get { lock (_sync) { return _snapshotTruncated; } } + set { lock (_sync) { _snapshotTruncated = value; } } + } /// Gets or sets the snapshots the scripted reply carries. - public IReadOnlyList Snapshots { get; init; } = []; + public IReadOnlyList Snapshots + { + get { lock (_sync) { return _snapshots; } } + set { lock (_sync) { _snapshots = value; } } + } /// Completes once the monitor has issued its first QueryActiveAlarms. /// The maximum time to wait. /// A task that represents the asynchronous operation. public Task WaitForReconcileAsync(TimeSpan timeout) => _reconciled.Task.WaitAsync(timeout); + /// Pushes a worker event into the session's distributor pump. + /// The event to push. + public void EmitEvent(MxEvent mxEvent) => + _events.Writer.TryWrite(new WorkerEvent { Event = mxEvent }); + + /// + /// 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. + /// + public void EndWorkerEventStream() => _events.Writer.TryComplete(); + /// public Task 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(); }