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
@@ -26,11 +26,34 @@ public class SiteAlarmLiveCacheServiceTests : TestKit
// A mock site-wide alarm stream client whose subscription hangs until cancelled.
private sealed class HangingClient : SiteStreamGrpcClient
{
private readonly object _lock = new();
private readonly List<Action> _connects = new();
private readonly List<Action> _completions = new();
public HangingClient() : base() { }
/// <summary>When false the site never accepts the subscription (no connect signal).</summary>
public bool AutoConnect { get; set; } = true;
/// <summary>Connect callbacks captured from every subscribe, for manual firing.</summary>
public List<Action> Connects { get { lock (_lock) { return _connects.ToList(); } } }
/// <summary>Graceful-completion callbacks captured from every subscribe.</summary>
public List<Action> Completions { get { lock (_lock) { return _completions.ToList(); } } }
public override Task SubscribeSiteAsync(
string correlationId, Action<Commons.Messages.Streaming.AlarmStateChanged> onAlarmEvent,
Action<Exception> onError, Action onCompleted, CancellationToken ct)
Action<Exception> onError, Action onCompleted, CancellationToken ct,
Action? onConnected = null)
{
lock (_lock)
{
if (onConnected is not null) _connects.Add(onConnected);
_completions.Add(onCompleted);
}
// A healthy site accepts the subscription immediately (headers flushed on
// subscribe), which is what makes the aggregator report the stream live.
if (AutoConnect) onConnected?.Invoke();
var tcs = new TaskCompletionSource();
ct.Register(() => tcs.TrySetResult());
return tcs.Task;
@@ -43,6 +66,9 @@ public class SiteAlarmLiveCacheServiceTests : TestKit
private readonly HangingClient _client = new();
public int GetOrCreateCount;
/// <summary>The single client this factory hands out, for connect/complete control.</summary>
public HangingClient Client => _client;
public CountingFactory() : base(NullLoggerFactory.Instance) { }
public override SiteStreamGrpcClient GetOrCreate(string siteIdentifier, string grpcEndpoint)
@@ -338,4 +364,43 @@ public class SiteAlarmLiveCacheServiceTests : TestKit
Thread.Sleep(300);
Assert.Equal(startsAfterStop, factory.GetOrCreateCount); // no self-heal restart fired
}
// ── WP2.3: IsLive tracks the STREAM, not merely "a snapshot was published once" ──
[Fact]
public void IsLive_StaysFalse_UntilTheSiteAcceptsTheStream()
{
var service = CreateService(TimeSpan.FromMilliseconds(200), out var factory);
factory.Client.AutoConnect = false; // site never answers the subscription
using var sub = service.Subscribe(SiteId, () => { });
// The (empty) seed completes and publishes, but with no accepted stream behind it
// the cache is not live — the page must keep polling. Pre-fix IsLive flipped true
// here and the page grafted a never-updating snapshot over fresh poll data.
AwaitCondition(() => service.GetCurrentAlarms(SiteId) is not null, TimeSpan.FromSeconds(5));
Thread.Sleep(400);
Assert.False(service.IsLive(SiteId));
// The site accepts it → live.
AwaitCondition(() => factory.Client.Connects.Count >= 1, TimeSpan.FromSeconds(5));
factory.Client.Connects[0]();
AwaitCondition(() => service.IsLive(SiteId), TimeSpan.FromSeconds(5));
}
[Fact]
public void IsLive_DropsWhenTheStreamCompletes()
{
// The carried residual from Phase 1: a stream that ended gracefully (the site's 4h
// max lifetime) left IsLive true until the next reconcile publish.
var service = CreateService(TimeSpan.FromMilliseconds(200), out var factory);
using var sub = service.Subscribe(SiteId, () => { });
AwaitCondition(() => service.IsLive(SiteId), TimeSpan.FromSeconds(5));
AwaitCondition(() => factory.Client.Completions.Count >= 1, TimeSpan.FromSeconds(5));
factory.Client.Completions[0]();
AwaitCondition(() => !service.IsLive(SiteId), TimeSpan.FromSeconds(5));
}
}