feat(alarms): feed-level snapshot_status truncation frame on StreamAlarms
This commit is contained in:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user