fix(kpi): untracked projected fold fetch — fold reads no longer flood the change tracker (plan R2-04 T1)
This commit is contained in:
@@ -101,8 +101,14 @@ public sealed class KpiHistoryRepository : IKpiHistoryRepository
|
||||
// fetch it and group IN MEMORY. This deliberately avoids provider-specific
|
||||
// DATEADD/DATEPART hour-truncation translation (SQLite in tests, SQL Server in prod),
|
||||
// keeping the hour-bucket arithmetic identical across providers and trivially correct.
|
||||
// Projected, untracked fetch: the fold only reads these six fields and never
|
||||
// mutates a KpiSample. A tracked ToListAsync registered ~5k-90k read-only
|
||||
// entities in the change tracker per healthy 3h fold — all re-scanned by
|
||||
// DetectChanges on the final SaveChanges (arch-review 04 round 2, R2). A
|
||||
// projection is inherently untracked and materializes no entity at all.
|
||||
var samples = await _context.KpiSamples
|
||||
.Where(s => s.CapturedAtUtc >= from && s.CapturedAtUtc < to)
|
||||
.Select(s => new FoldSample(s.Source, s.Metric, s.Scope, s.ScopeKey, s.CapturedAtUtc, s.Value))
|
||||
.ToListAsync(cancellationToken);
|
||||
if (samples.Count == 0)
|
||||
{
|
||||
@@ -212,4 +218,8 @@ public sealed class KpiHistoryRepository : IKpiHistoryRepository
|
||||
/// <summary>In-memory grouping key: one KPI series (four-tuple) within one UTC hour.</summary>
|
||||
private readonly record struct SeriesHourKey(
|
||||
string Source, string Metric, string Scope, string? ScopeKey, DateTime HourStartUtc);
|
||||
|
||||
/// <summary>Narrow, untracked projection of one KpiSample row for the fold (R2).</summary>
|
||||
private readonly record struct FoldSample(
|
||||
string Source, string Metric, string Scope, string? ScopeKey, DateTime CapturedAtUtc, double Value);
|
||||
}
|
||||
|
||||
+20
@@ -386,6 +386,26 @@ public class KpiHistoryRepositoryTests
|
||||
Assert.Equal(new[] { 3d, 4d }, remaining.Select(p => p.Value).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FoldHourlyRollups_DoesNotTrack_KpiSampleEntities()
|
||||
{
|
||||
await using var ctx = NewContext();
|
||||
var repo = new KpiHistoryRepository(ctx);
|
||||
await repo.RecordSamplesAsync(new[]
|
||||
{
|
||||
Sample("NotificationOutbox", "queueDepth", "Global", null, 5, Base.AddMinutes(10)),
|
||||
Sample("NotificationOutbox", "queueDepth", "Global", null, 7, Base.AddMinutes(50)),
|
||||
});
|
||||
ctx.ChangeTracker.Clear(); // isolate the fold's tracking behavior from the seed
|
||||
|
||||
await repo.FoldHourlyRollupsAsync(Base, Base.AddHours(1));
|
||||
|
||||
// The fold READS samples and WRITES rollups; the read must not register
|
||||
// thousands of read-only KpiSample entries for DetectChanges to re-scan
|
||||
// (arch-review 04 round 2, R2).
|
||||
Assert.Empty(ctx.ChangeTracker.Entries<KpiSample>());
|
||||
}
|
||||
|
||||
// Local mirror of the repo's private hour-truncation, for cutoff assertions.
|
||||
private static DateTime TruncateHour(DateTime utc) =>
|
||||
new(utc.Year, utc.Month, utc.Day, utc.Hour, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
Reference in New Issue
Block a user