perf(detect-anomalies): batch snapshot loads into a single query (HIGH)
DetectAnomaliesUseCase was issuing one ISnapshotRepository.ListByEventAsync call per event each cycle, with each call rehydrating that event's bets via Include(s => s.Bets) — O(N) SQLite round-trips and N Include payloads on every detection cycle. * Add ISnapshotRepository.ListByEventsAsync(IReadOnlyCollection<EventId>, …) returning a per-event dictionary; events with no snapshots in range get Array.Empty<OddsSnapshot>() so the caller doesn't need a presence check. * Implementation uses a single .Where(s => ids.Contains(s.EventCode)) query and groups in memory. * DetectAnomaliesUseCase loads the whole batch once before the foreach, then ProcessEventAsync receives the per-event slice as a parameter. * Tests updated to stub the new method; per-event-failure test now exercises an AddAsync throw rather than a snapshot-load throw, since individual snapshot loads no longer fail per-event.
This commit is contained in:
@@ -38,6 +38,43 @@ internal sealed class SnapshotRepository : ISnapshotRepository
|
||||
return entities.Select(Mapping.ToDomain).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<EventId, IReadOnlyList<OddsSnapshot>>> ListByEventsAsync(
|
||||
IReadOnlyCollection<EventId> eventIds,
|
||||
DateTimeOffset from,
|
||||
DateTimeOffset to,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(eventIds);
|
||||
|
||||
var result = new Dictionary<EventId, IReadOnlyList<OddsSnapshot>>(eventIds.Count);
|
||||
if (eventIds.Count == 0)
|
||||
return result;
|
||||
|
||||
var ids = eventIds.Select(e => e.Value).Distinct().ToArray();
|
||||
var fromStr = from.ToString("O");
|
||||
var toStr = to.ToString("O");
|
||||
|
||||
var entities = await _db.Snapshots.AsNoTracking()
|
||||
.Include(s => s.Bets)
|
||||
.Where(s => ids.Contains(s.EventCode)
|
||||
&& s.CapturedAt.CompareTo(fromStr) >= 0
|
||||
&& s.CapturedAt.CompareTo(toStr) <= 0)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var grouped = entities
|
||||
.GroupBy(e => e.EventCode)
|
||||
.ToDictionary(g => g.Key, g => g.Select(Mapping.ToDomain).ToList());
|
||||
|
||||
foreach (var id in eventIds)
|
||||
{
|
||||
result[id] = grouped.TryGetValue(id.Value, out var list)
|
||||
? list.AsReadOnly()
|
||||
: Array.Empty<OddsSnapshot>();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task AddAsync(OddsSnapshot entity, CancellationToken ct = default)
|
||||
{
|
||||
var efEntity = Mapping.ToEntity(entity);
|
||||
|
||||
Reference in New Issue
Block a user