fix(site-call-audit): KPI snapshots as single conditional-aggregate queries (review 2026-10-21 06/PERF-3, 08/P3)

This commit is contained in:
Joseph Doherty
2026-10-22 09:23:53 -04:00
parent 811a561885
commit e7fb71c2de
3 changed files with 240 additions and 145 deletions
@@ -415,47 +415,55 @@ OPTION (RECOMPILE);";
private const string StatusDelivered = "Delivered";
private const string StatusFailed = "Failed";
/// <summary>
/// Restricts a KPI aggregation to the rows that contribute to at least one metric:
/// every non-terminal row (buffered / stuck / oldest-pending), every Parked row, and
/// Delivered/Failed rows that went terminal inside the interval. The historical
/// terminal bulk of the table contributes to none of them, so the aggregation's cost
/// tracks the working set rather than retention, and a site/node with only uncounted
/// rows does not surface as an all-zero per-scope row (preserving the old
/// union-of-metric-keys semantics). The filtered <c>IX_SiteCalls_NonTerminal</c> /
/// <c>IX_SiteCalls_Terminal</c> indexes cover the legs.
/// </summary>
private IQueryable<SiteCall> KpiContributingRows(DateTime intervalSince) =>
_context.SiteCalls.Where(s => s.TerminalAtUtc == null
|| s.Status == StatusParked
|| ((s.Status == StatusDelivered || s.Status == StatusFailed)
&& s.TerminalAtUtc >= intervalSince));
/// <inheritdoc />
public async Task<SiteCallKpiSnapshot> ComputeKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default)
{
var now = DateTime.UtcNow;
var bufferedCount = await _context.SiteCalls
.CountAsync(s => s.TerminalAtUtc == null, ct);
var parkedCount = await _context.SiteCalls
.CountAsync(s => s.Status == StatusParked, ct);
var failedLastInterval = await _context.SiteCalls
.CountAsync(s => s.Status == StatusFailed
&& s.TerminalAtUtc != null
&& s.TerminalAtUtc >= intervalSince, ct);
var deliveredLastInterval = await _context.SiteCalls
.CountAsync(s => s.Status == StatusDelivered
&& s.TerminalAtUtc != null
&& s.TerminalAtUtc >= intervalSince, ct);
var stuckCount = await _context.SiteCalls
.CountAsync(s => s.TerminalAtUtc == null && s.CreatedAtUtc < stuckCutoff, ct);
var nonTerminal = _context.SiteCalls.Where(s => s.TerminalAtUtc == null);
TimeSpan? oldestPendingAge = null;
if (await nonTerminal.AnyAsync(ct))
{
var oldestCreatedAt = await nonTerminal.MinAsync(s => s.CreatedAtUtc, ct);
oldestPendingAge = now - oldestCreatedAt;
}
// One conditional-aggregation pass replaces the five sequential COUNTs + Any +
// Min round trips (review 2026-10-21 06/PERF-3, 08/P3), mirroring
// NotificationOutboxRepository.ComputeKpisAsync. GroupBy(_ => 1) yields a single
// group (no contributing rows → no group → all zeros). The oldest non-terminal
// CreatedAtUtc is a MIN over a CASE in the same scan.
var counts = await KpiContributingRows(intervalSince)
.GroupBy(_ => 1)
.Select(g => new
{
Buffered = g.Count(s => s.TerminalAtUtc == null),
Parked = g.Count(s => s.Status == StatusParked),
Failed = g.Count(s => s.Status == StatusFailed
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince),
Delivered = g.Count(s => s.Status == StatusDelivered
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince),
Stuck = g.Count(s => s.TerminalAtUtc == null && s.CreatedAtUtc < stuckCutoff),
OldestCreatedAt = g.Min(s => s.TerminalAtUtc == null ? (DateTime?)s.CreatedAtUtc : null),
})
.FirstOrDefaultAsync(ct);
return new SiteCallKpiSnapshot(
BufferedCount: bufferedCount,
ParkedCount: parkedCount,
FailedLastInterval: failedLastInterval,
DeliveredLastInterval: deliveredLastInterval,
OldestPendingAge: oldestPendingAge,
StuckCount: stuckCount);
BufferedCount: counts?.Buffered ?? 0,
ParkedCount: counts?.Parked ?? 0,
FailedLastInterval: counts?.Failed ?? 0,
DeliveredLastInterval: counts?.Delivered ?? 0,
OldestPendingAge: counts?.OldestCreatedAt is { } oldest ? now - oldest : null,
StuckCount: counts?.Stuck ?? 0);
}
/// <inheritdoc />
@@ -464,45 +472,35 @@ OPTION (RECOMPILE);";
{
var now = DateTime.UtcNow;
var buffered = await CountBySiteAsync(s => s.TerminalAtUtc == null, ct);
// One grouped conditional-aggregation query (counts + MIN) instead of five
// grouped COUNTs + a grouped MIN (review 2026-10-21 06/PERF-3, 08/P3).
var rows = await KpiContributingRows(intervalSince)
.GroupBy(s => s.SourceSite)
.Select(g => new
{
Site = g.Key,
Buffered = g.Count(s => s.TerminalAtUtc == null),
Parked = g.Count(s => s.Status == StatusParked),
Failed = g.Count(s => s.Status == StatusFailed
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince),
Delivered = g.Count(s => s.Status == StatusDelivered
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince),
Stuck = g.Count(s => s.TerminalAtUtc == null && s.CreatedAtUtc < stuckCutoff),
OldestCreatedAt = g.Min(s => s.TerminalAtUtc == null ? (DateTime?)s.CreatedAtUtc : null),
})
.ToListAsync(ct);
var parked = await CountBySiteAsync(s => s.Status == StatusParked, ct);
var failed = await CountBySiteAsync(
s => s.Status == StatusFailed
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince, ct);
var delivered = await CountBySiteAsync(
s => s.Status == StatusDelivered
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince, ct);
var stuck = await CountBySiteAsync(
s => s.TerminalAtUtc == null && s.CreatedAtUtc < stuckCutoff, ct);
// Oldest non-terminal CreatedAtUtc per site — a server-side GROUP BY MIN.
var oldest = (await _context.SiteCalls
.Where(s => s.TerminalAtUtc == null)
.GroupBy(s => s.SourceSite)
.Select(g => new { Site = g.Key, Oldest = g.Min(s => s.CreatedAtUtc) })
.ToListAsync(ct))
.ToDictionary(x => x.Site, x => x.Oldest);
var siteIds = buffered.Keys
.Concat(parked.Keys).Concat(failed.Keys)
.Concat(delivered.Keys).Concat(stuck.Keys)
.Distinct()
.OrderBy(s => s, StringComparer.Ordinal);
return siteIds.Select(site => new SiteCallSiteKpiSnapshot(
SourceSite: site,
BufferedCount: buffered.GetValueOrDefault(site),
ParkedCount: parked.GetValueOrDefault(site),
FailedLastInterval: failed.GetValueOrDefault(site),
DeliveredLastInterval: delivered.GetValueOrDefault(site),
OldestPendingAge: oldest.TryGetValue(site, out var createdAt)
? now - createdAt
: null,
StuckCount: stuck.GetValueOrDefault(site))).ToList();
return rows
.OrderBy(r => r.Site, StringComparer.Ordinal)
.Select(r => new SiteCallSiteKpiSnapshot(
SourceSite: r.Site,
BufferedCount: r.Buffered,
ParkedCount: r.Parked,
FailedLastInterval: r.Failed,
DeliveredLastInterval: r.Delivered,
OldestPendingAge: r.OldestCreatedAt is { } oldest ? now - oldest : null,
StuckCount: r.Stuck))
.ToList();
}
/// <inheritdoc />
@@ -511,81 +509,37 @@ OPTION (RECOMPILE);";
{
var now = DateTime.UtcNow;
// Exclude rows with NULL SourceNode — per-node KPIs are only meaningful
// when the node identity is known. Each predicate guards n.SourceNode != null
// so the GROUP BY key is always non-null.
var buffered = await CountByNodeAsync(
s => s.TerminalAtUtc == null && s.SourceNode != null, ct);
var parked = await CountByNodeAsync(
s => s.Status == StatusParked && s.SourceNode != null, ct);
var failed = await CountByNodeAsync(
s => s.Status == StatusFailed
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince
&& s.SourceNode != null, ct);
var delivered = await CountByNodeAsync(
s => s.Status == StatusDelivered
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince
&& s.SourceNode != null, ct);
var stuck = await CountByNodeAsync(
s => s.TerminalAtUtc == null && s.CreatedAtUtc < stuckCutoff
&& s.SourceNode != null, ct);
// Oldest non-terminal CreatedAtUtc per node — server-side GROUP BY MIN.
var oldest = (await _context.SiteCalls
.Where(s => s.TerminalAtUtc == null && s.SourceNode != null)
.GroupBy(s => s.SourceNode!)
.Select(g => new { Node = g.Key, Oldest = g.Min(s => s.CreatedAtUtc) })
.ToListAsync(ct))
.ToDictionary(x => x.Node, x => x.Oldest);
var nodeNames = buffered.Keys
.Concat(parked.Keys).Concat(failed.Keys)
.Concat(delivered.Keys).Concat(stuck.Keys)
.Distinct()
.OrderBy(n => n, StringComparer.Ordinal);
return nodeNames.Select(node => new SiteCallNodeKpiSnapshot(
SourceNode: node,
BufferedCount: buffered.GetValueOrDefault(node),
ParkedCount: parked.GetValueOrDefault(node),
FailedLastInterval: failed.GetValueOrDefault(node),
DeliveredLastInterval: delivered.GetValueOrDefault(node),
OldestPendingAge: oldest.TryGetValue(node, out var createdAt)
? now - createdAt
: null,
StuckCount: stuck.GetValueOrDefault(node))).ToList();
}
/// <summary>Counts <c>SiteCalls</c> rows matching <paramref name="predicate"/>, grouped by source site.</summary>
private async Task<Dictionary<string, int>> CountBySiteAsync(
System.Linq.Expressions.Expression<Func<SiteCall, bool>> predicate,
CancellationToken ct)
{
return await _context.SiteCalls
.Where(predicate)
.GroupBy(s => s.SourceSite)
.Select(g => new { Site = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.Site, x => x.Count, ct);
}
/// <summary>
/// Counts <c>SiteCalls</c> rows matching <paramref name="predicate"/>, grouped by source node.
/// Only rows with a non-null <c>SourceNode</c> should be included; the predicate is
/// responsible for enforcing that guard.
/// </summary>
private async Task<Dictionary<string, int>> CountByNodeAsync(
System.Linq.Expressions.Expression<Func<SiteCall, bool>> predicate,
CancellationToken ct)
{
return await _context.SiteCalls
.Where(predicate)
// Same single grouped aggregation as ComputePerSiteKpisAsync, keyed by
// SourceNode. Rows with a NULL SourceNode are excluded — per-node KPIs are only
// meaningful when the node identity is known.
var rows = await KpiContributingRows(intervalSince)
.Where(s => s.SourceNode != null)
.GroupBy(s => s.SourceNode!)
.Select(g => new { Node = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.Node, x => x.Count, ct);
.Select(g => new
{
Node = g.Key,
Buffered = g.Count(s => s.TerminalAtUtc == null),
Parked = g.Count(s => s.Status == StatusParked),
Failed = g.Count(s => s.Status == StatusFailed
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince),
Delivered = g.Count(s => s.Status == StatusDelivered
&& s.TerminalAtUtc != null && s.TerminalAtUtc >= intervalSince),
Stuck = g.Count(s => s.TerminalAtUtc == null && s.CreatedAtUtc < stuckCutoff),
OldestCreatedAt = g.Min(s => s.TerminalAtUtc == null ? (DateTime?)s.CreatedAtUtc : null),
})
.ToListAsync(ct);
return rows
.OrderBy(r => r.Node, StringComparer.Ordinal)
.Select(r => new SiteCallNodeKpiSnapshot(
SourceNode: r.Node,
BufferedCount: r.Buffered,
ParkedCount: r.Parked,
FailedLastInterval: r.Failed,
DeliveredLastInterval: r.Delivered,
OldestPendingAge: r.OldestCreatedAt is { } oldest ? now - oldest : null,
StuckCount: r.Stuck))
.ToList();
}
private static int GetRankOrThrow(string status)