perf(comms): alarms-only seed, capped buffers, at-least-once audit pull

This commit is contained in:
Joseph Doherty
2026-08-14 21:10:19 -04:00
parent 1040dc0fcc
commit 2ce0ad7ed1
30 changed files with 1941 additions and 482 deletions
@@ -132,7 +132,7 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
public bool IsLive(int siteId)
{
lock (_lock)
return _sites.TryGetValue(siteId, out var entry) && entry.HasPublished;
return _sites.TryGetValue(siteId, out var entry) && entry.HasPublished && entry.StreamLive;
}
// ── Subscriber teardown ─────────────────────────────────────────────────────
@@ -213,8 +213,8 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
Func<CancellationToken, Task<IReadOnlyList<AlarmStateChanged>>> seedFn =
ct => FanOutSnapshotsAsync(entry.SiteId, ct);
Action<IReadOnlyList<AlarmStateChanged>> publish =
snapshot => OnPublish(entry, snapshot);
Action<IReadOnlyList<AlarmStateChanged>, bool> publish =
(snapshot, streamLive) => OnPublish(entry, snapshot, streamLive);
lock (_lock)
{
@@ -236,7 +236,8 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
_options.LiveAlarmCacheReconcileInterval,
_options.LiveAlarmCachePublishCoalesce,
TimeSpan.FromSeconds(5), // reconnect delay — former ReconnectDelay static default
TimeSpan.FromSeconds(60))); // stability window — former StabilityWindow static default
TimeSpan.FromSeconds(60), // stability window — former StabilityWindow static default
_options.LiveAlarmCacheReconcileJitterFraction));
entry.Actor = system.ActorOf(props, $"site-alarm-aggregator-{entry.SiteId}-{Guid.NewGuid():N}");
@@ -378,7 +379,12 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
await gate.WaitAsync(ct);
try
{
var request = new DebugSnapshotRequest(instanceUniqueName, Guid.NewGuid().ToString("N"));
// Alarms-only (WP2.3): this fan-out discards snapshot.AttributeValues, so
// asking the site to build and ship them is pure wire waste. The flag is
// additive on the wire; a pre-WP2.3 site ignores it and returns the full
// snapshot, which this loop reads exactly as before.
var request = new DebugSnapshotRequest(
instanceUniqueName, Guid.NewGuid().ToString("N"), AlarmsOnly: true);
var snapshot = await _communicationService.RequestDebugSnapshotAsync(siteIdentifier, request, ct);
if (snapshot.InstanceNotFound)
return;
@@ -402,7 +408,7 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
// ── Publish (actor thread → viewers) ────────────────────────────────────────
private void OnPublish(SiteEntry entry, IReadOnlyList<AlarmStateChanged> snapshot)
private void OnPublish(SiteEntry entry, IReadOnlyList<AlarmStateChanged> snapshot, bool streamLive)
{
Subscription[] subscribers;
lock (_lock)
@@ -410,6 +416,10 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
// Store the fresh immutable snapshot (readers get it lock-free-ish via GetCurrentAlarms).
entry.Current = snapshot;
entry.HasPublished = true;
// Liveness is the STREAM's, not the cache's: a completed or given-up stream must
// stop reporting live between reopen ticks, or the page grafts a freezing snapshot
// over fresh poll data (WP2.3 carried residual).
entry.StreamLive = streamLive;
subscribers = entry.Subscribers.ToArray();
}
@@ -462,6 +472,7 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
entry.Actor = null;
entry.HasPublished = false;
entry.StreamLive = false;
entry.Current = Empty;
if (entry.Subscribers.Count > 0 && !entry.Starting)
@@ -498,6 +509,13 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
/// <summary>True once the aggregator has seeded and published at least once.</summary>
public bool HasPublished { get; set; }
/// <summary>
/// Liveness of the aggregator's site-wide gRPC stream as of the last publish. Both
/// this and <see cref="HasPublished"/> must hold for <c>IsLive</c>: a published cache
/// behind a dead stream is stale, not live.
/// </summary>
public bool StreamLive { get; set; }
public Timer? LingerTimer { get; set; }
public int LingerVersion { get; set; }