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
@@ -121,6 +121,105 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
Assert.Equal(3, count);
}
[SkippableFact]
public async Task Receive_DuplicateEventIds_WithinOnePacket_ProduceOneRow_AndAreAllAcked()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
// WP2.2 writes each packet as ONE set-based statement, whose anti-semi-join
// only sees already-COMMITTED rows. Two copies of an EventId inside one
// packet would therefore both pass it and collide on the clustered PK,
// taking the whole packet down — the repository de-duplicates first.
// Every id is still acked: the site's contract is "this row is now
// present at central", which is true for both copies.
var siteId = NewSiteId();
var repeated = NewEvent(siteId);
var other = NewEvent(siteId);
var batch = new List<AuditEvent> { repeated, other, repeated, repeated };
await using var context = CreateContext();
var repo = new AuditLogRepository(context);
var actor = CreateActor(repo);
actor.Tell(new IngestAuditEventsCommand(batch), TestActor);
var reply = ExpectMsg<IngestAuditEventsReply>(TimeSpan.FromSeconds(10));
Assert.Equal(4, reply.AcceptedEventIds.Count);
Assert.True(
new[] { repeated.EventId, other.EventId }.ToHashSet()
.SetEquals(reply.AcceptedEventIds.ToHashSet()));
await using var readContext = CreateContext();
var rows = await readContext.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(2, rows.Count);
}
[SkippableFact]
public async Task Receive_SamePacketTwice_IsIdempotent_AcrossPackets()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
// A site whose ack was lost re-delivers the identical packet on the next
// drain, and the reconciliation pull can re-deliver it a third time. Each
// replay must ack fully (so the site can finally flip its rows to
// Forwarded) while writing nothing new.
var siteId = NewSiteId();
var events = Enumerable.Range(0, 6).Select(_ => NewEvent(siteId)).ToList();
await using var context = CreateContext();
var repo = new AuditLogRepository(context);
var actor = CreateActor(repo);
for (var attempt = 0; attempt < 3; attempt++)
{
actor.Tell(new IngestAuditEventsCommand(events), TestActor);
var reply = ExpectMsg<IngestAuditEventsReply>(TimeSpan.FromSeconds(10));
Assert.Equal(6, reply.AcceptedEventIds.Count);
Assert.True(
events.Select(e => e.EventId).ToHashSet()
.SetEquals(reply.AcceptedEventIds.ToHashSet()));
}
await using var readContext = CreateContext();
var rows = await readContext.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(6, rows.Count);
}
[SkippableFact]
public async Task Receive_OverlappingPackets_InsertOnlyTheNewRows()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
// Partially-overlapping packets are the normal reconciliation shape: the
// pull cursor re-serves a tail the push already delivered. The overlap
// must be a silent no-op and the new rows must land.
var siteId = NewSiteId();
var first = Enumerable.Range(0, 4).Select(_ => NewEvent(siteId)).ToList();
var second = first.Skip(2).Concat(
Enumerable.Range(0, 3).Select(_ => NewEvent(siteId))).ToList();
await using var context = CreateContext();
var repo = new AuditLogRepository(context);
var actor = CreateActor(repo);
actor.Tell(new IngestAuditEventsCommand(first), TestActor);
ExpectMsg<IngestAuditEventsReply>(TimeSpan.FromSeconds(10));
actor.Tell(new IngestAuditEventsCommand(second), TestActor);
var reply = ExpectMsg<IngestAuditEventsReply>(TimeSpan.FromSeconds(10));
Assert.Equal(5, reply.AcceptedEventIds.Count);
await using var readContext = CreateContext();
var rows = await readContext.Set<AuditLogRow>()
.Where(e => e.SourceSiteId == siteId)
.ToListAsync();
Assert.Equal(7, rows.Count);
}
[SkippableFact]
public async Task Receive_Sets_IngestedAtUtc_Before_Insert()
{