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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user