perf(kpi-history): time-sliced batched purge replaces the single daily mega-DELETE

This commit is contained in:
Joseph Doherty
2026-07-09 09:06:42 -04:00
parent 5332912f26
commit 71189298a2
4 changed files with 116 additions and 5 deletions
@@ -42,7 +42,9 @@ public interface IKpiHistoryRepository
/// <summary>
/// Bulk-deletes rows whose <see cref="KpiSample.CapturedAtUtc"/> is strictly older
/// than <paramref name="before"/> (retention purge).
/// than <paramref name="before"/> (retention purge). Implementations slice the
/// purge into time-windowed DELETE batches so a large catch-up (e.g. after an
/// outage) never runs as a single lock/log-heavy transaction (arch-review 04, P4).
/// </summary>
/// <param name="before">UTC cut-off; rows captured before this instant are deleted.</param>
/// <param name="cancellationToken">Cancellation token.</param>
@@ -64,9 +64,22 @@ public sealed class KpiHistoryRepository : IKpiHistoryRepository
/// <inheritdoc />
public async Task<int> PurgeOlderThanAsync(DateTime before, CancellationToken cancellationToken = default)
{
// Set-based delete — no entity materialisation; returns the rows affected.
return await _context.KpiSamples
.Where(s => s.CapturedAtUtc < before)
.ExecuteDeleteAsync(cancellationToken);
// Time-sliced batches: each DELETE covers at most one hour of samples, capping the
// lock/log footprint per statement (arch-review 04, P4 — steady state is ~1 day of
// rows/day; after an outage the catch-up would otherwise be one giant transaction).
var total = 0;
var floor = await _context.KpiSamples.Where(s => s.CapturedAtUtc < before)
.MinAsync(s => (DateTime?)s.CapturedAtUtc, cancellationToken);
while (floor is not null && floor < before)
{
var ceiling = floor.Value.AddHours(1) < before ? floor.Value.AddHours(1) : before;
total += await _context.KpiSamples
.Where(s => s.CapturedAtUtc < ceiling)
.ExecuteDeleteAsync(cancellationToken);
floor = await _context.KpiSamples.Where(s => s.CapturedAtUtc < before)
.MinAsync(s => (DateTime?)s.CapturedAtUtc, cancellationToken);
}
return total;
}
}