perf(comms+audit): close phase-2 residuals — direct ingest path, monotonic timeouts, synthetic probe, not-reporting set, cursor-exact audit pull

This commit is contained in:
Joseph Doherty
2026-08-14 21:38:23 -04:00
parent 4cd1441984
commit a5882753dd
38 changed files with 1254 additions and 443 deletions
@@ -331,6 +331,14 @@ public class AlarmSummaryRenderTests : BunitContext
public bool IsLive(int siteId) => _live;
/// <summary>
/// The aggregator's own fan-out result. Settable so a test can prove the page renders
/// the not-reporting list sourced from the live cache while it is serving the site.
/// </summary>
public IReadOnlyList<string> NotReporting { get; set; } = Array.Empty<string>();
public IReadOnlyList<string> GetNotReportingInstances(int siteId) => NotReporting;
public void PushAlarms(IReadOnlyList<AlarmStateChanged> alarms)
{
_current = alarms;
@@ -105,6 +105,8 @@ public class AlarmSummaryVirtualizeTests : BunitContext
public bool IsLive(int siteId) => false;
public IReadOnlyList<string> GetNotReportingInstances(int siteId) => Array.Empty<string>();
private sealed class NoOp : IDisposable
{
public void Dispose() { }
@@ -56,13 +56,13 @@ public class SharedAlarmSummaryServiceTests : IDisposable
_provider = services.BuildServiceProvider();
}
private SharedAlarmSummaryService CreateSut(TimeSpan liveCacheTtl) =>
new(_provider.GetRequiredService<IServiceScopeFactory>(), _liveCache, liveCacheTtl, () => _now);
private SharedAlarmSummaryService CreateSut() =>
new(_provider.GetRequiredService<IServiceScopeFactory>(), _liveCache, () => _now);
[Fact]
public async Task ConcurrentCircuits_ShareOneFanOut()
{
var sut = CreateSut(TimeSpan.FromSeconds(60));
var sut = CreateSut();
var results = await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => sut.GetSiteAlarmsAsync(SiteId)));
@@ -73,7 +73,7 @@ public class SharedAlarmSummaryServiceTests : IDisposable
[Fact]
public async Task ColdLiveCache_RefreshesWithinThePageTick()
{
var sut = CreateSut(TimeSpan.FromSeconds(60));
var sut = CreateSut();
_liveCache.Live = false;
await sut.GetSiteAlarmsAsync(SiteId);
@@ -86,22 +86,51 @@ public class SharedAlarmSummaryServiceTests : IDisposable
}
[Fact]
public async Task LiveCacheServing_WidensTheWindowToTheReconcileInterval()
public async Task LiveCacheServing_SkipsTheFanOutEntirely()
{
var sut = CreateSut(TimeSpan.FromSeconds(60));
// The aggregator's seed/reconcile already ran this exact fan-out and publishes both
// halves of the answer, so the façade must not run a second one — not now, not after
// any elapsed window (arch-review phase-2 residual #4).
var sut = CreateSut();
_liveCache.Live = true;
_liveCache.NotReporting = new[] { "inst-silent" };
_liveCache.Current = new[]
{
new AlarmStateChanged("inst-a", "A-alarm", AlarmState.Active, 500, T0),
};
var first = await sut.GetSiteAlarmsAsync(SiteId);
_now = T0.AddSeconds(61);
var second = await sut.GetSiteAlarmsAsync(SiteId);
await _instanceRepo.DidNotReceive().GetInstancesBySiteIdAsync(SiteId, Arg.Any<CancellationToken>());
await _snapshotClient.DidNotReceive().GetSnapshotAsync(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
// Both halves come from the cache: the rows so a liveness flip mid-call lands on
// last-known state instead of a blank grid, and the not-reporting names the page shows.
foreach (var result in new[] { first, second })
{
Assert.Equal("A-alarm", Assert.Single(result.Alarms).Alarm.AlarmName);
Assert.Equal("inst-silent", Assert.Single(result.NotReportingInstances));
}
}
[Fact]
public async Task LiveCacheGoingCold_FallsBackToTheFanOut()
{
var sut = CreateSut();
_liveCache.Live = true;
await sut.GetSiteAlarmsAsync(SiteId);
_now = T0.AddSeconds(30);
await sut.GetSiteAlarmsAsync(SiteId);
await _instanceRepo.DidNotReceive().GetInstancesBySiteIdAsync(SiteId, Arg.Any<CancellationToken>());
// Aggregator died / stream degraded → the poll is the page's rebuild path again.
_liveCache.Live = false;
var result = await sut.GetSiteAlarmsAsync(SiteId);
// Live deltas own the rows; only the not-reporting list still comes from the
// fan-out, so a 30s-old answer is fine and costs no second fan-out.
await _instanceRepo.Received(1).GetInstancesBySiteIdAsync(SiteId, Arg.Any<CancellationToken>());
_now = T0.AddSeconds(61);
await sut.GetSiteAlarmsAsync(SiteId);
await _instanceRepo.Received(2).GetInstancesBySiteIdAsync(SiteId, Arg.Any<CancellationToken>());
Assert.Single(result.Alarms);
}
[Fact]
@@ -113,7 +142,7 @@ public class SharedAlarmSummaryServiceTests : IDisposable
_instanceRepo.GetInstancesBySiteIdAsync(otherSite, Arg.Any<CancellationToken>())
.Returns(new List<Instance>());
var sut = CreateSut(TimeSpan.FromSeconds(60));
var sut = CreateSut();
await sut.GetSiteAlarmsAsync(SiteId);
await sut.GetSiteAlarmsAsync(otherSite);
@@ -125,7 +154,7 @@ public class SharedAlarmSummaryServiceTests : IDisposable
[Fact]
public void PureMethods_MatchTheDirectImplementation()
{
var sut = CreateSut(TimeSpan.FromSeconds(60));
var sut = CreateSut();
var direct = new AlarmSummaryService(_instanceRepo, _siteRepo, _snapshotClient);
var alarms = new List<AlarmStateChanged>
{
@@ -149,18 +178,24 @@ public class SharedAlarmSummaryServiceTests : IDisposable
public void Dispose() => _provider.Dispose();
/// <summary>Liveness-only stub — the façade consults nothing else on the live cache.</summary>
/// <summary>
/// Read-side stub: liveness, the published alarm snapshot, and the aggregator's
/// not-reporting set — the three things the façade reads while the cache is serving a site.
/// </summary>
private sealed class FakeLiveCache : ISiteAlarmLiveCache
{
public bool Live { get; set; }
public IReadOnlyList<AlarmStateChanged> Current { get; set; } = Array.Empty<AlarmStateChanged>();
public IReadOnlyList<string> NotReporting { get; set; } = Array.Empty<string>();
public IDisposable Subscribe(int siteId, Action onChanged) => new NoOp();
public IReadOnlyList<AlarmStateChanged> GetCurrentAlarms(int siteId) =>
Array.Empty<AlarmStateChanged>();
public IReadOnlyList<AlarmStateChanged> GetCurrentAlarms(int siteId) => Current;
public bool IsLive(int siteId) => Live;
public IReadOnlyList<string> GetNotReportingInstances(int siteId) => NotReporting;
private sealed class NoOp : IDisposable
{
public void Dispose() { }