perf(central): set-based ingest, aligned partition purge, KPI query shapes, EF hygiene

This commit is contained in:
Joseph Doherty
2026-08-14 21:07:12 -04:00
parent ee193cd2bb
commit 5db2a810c0
29 changed files with 3790 additions and 266 deletions
@@ -128,6 +128,37 @@ public sealed class KpiHistoryRepository : IKpiHistoryRepository
var groups = samples.GroupBy(s => new SeriesHourKey(
s.Source, s.Metric, s.Scope, s.ScopeKey, TruncateToHour(s.CapturedAtUtc)));
// Preload every rollup row already covering this window in ONE query and
// index it by series+hour (WP2.2). The predecessor issued a
// FirstOrDefaultAsync existence probe PER (series, hour) group — an N+1
// that scaled with the metric catalogue times the lookback: a 3 h re-fold
// over ~40 series cost ~120 sequential round trips before a single row was
// written. The window is bounded by the caller's small trailing lookback
// and the rollup table holds exactly one row per series-hour, so the
// preload is a narrow range seek on IX_KpiRollupHourly_Series.
//
// Deliberately TRACKED (not a projection): the re-fold path mutates the
// existing entity in place and relies on the change tracker to emit the
// UPDATE. The dictionary's ScopeKey comparison is ordinal where the
// previous SQL predicate used the database collation; both sides are
// written from the same KpiSample.ScopeKey values, so they are
// byte-identical in practice and a residual mismatch degrades to the
// already-handled upsert-race path rather than a wrong aggregate.
var existingRollups = await _context.KpiRollupHourly
.Where(r => r.HourStartUtc >= from && r.HourStartUtc < to)
.ToListAsync(cancellationToken);
var existingByKey = new Dictionary<SeriesHourKey, KpiRollupHourly>(existingRollups.Count);
foreach (var row in existingRollups)
{
existingByKey[new SeriesHourKey(
row.Source,
row.Metric,
row.Scope,
row.ScopeKey,
DateTime.SpecifyKind(row.HourStartUtc, DateTimeKind.Utc))] = row;
}
foreach (var group in groups)
{
var key = group.Key;
@@ -143,18 +174,11 @@ public sealed class KpiHistoryRepository : IKpiHistoryRepository
var maxValue = group.Max(s => s.Value);
var sampleCount = group.Count();
// Idempotent upsert on the unique series+hour key. The ScopeKey == key.ScopeKey
// comparison matches null against the Global-scope rows (IS NULL) exactly as the
// UNIQUE IX_KpiRollupHourly_Series index treats a null key as participating.
var existing = await _context.KpiRollupHourly.FirstOrDefaultAsync(
r => r.Source == key.Source
&& r.Metric == key.Metric
&& r.Scope == key.Scope
&& r.ScopeKey == key.ScopeKey
&& r.HourStartUtc == key.HourStartUtc,
cancellationToken);
if (existing is null)
// Idempotent upsert on the unique series+hour key, resolved against the
// preloaded dictionary. A null ScopeKey keys the Global-scope rows exactly
// as the UNIQUE IX_KpiRollupHourly_Series index treats a null key as
// participating.
if (!existingByKey.TryGetValue(key, out var existing))
{
_context.KpiRollupHourly.Add(new KpiRollupHourly
{