perf(comms): alarms-only seed, capped buffers, at-least-once audit pull

This commit is contained in:
Joseph Doherty
2026-08-14 21:10:19 -04:00
parent 1040dc0fcc
commit 2ce0ad7ed1
30 changed files with 1941 additions and 482 deletions
@@ -531,6 +531,123 @@ public class SqliteAuditWriterWriteTests
Assert.Equal(pending.EventId, rows[0].EventId);
}
// ----- WP2.3: composite keyset cursor + cursor-proved retirement ----- //
[Fact]
public async Task ReadPendingSinceAsync_WithAfterId_SkipsRowsAtOrBeforeTheCompositeCursor()
{
// A batch of rows sharing ONE exact instant used to pin the inclusive-timestamp
// cursor forever: every pull re-served the same page and the backlog never drained.
var (writer, _) = CreateWriter(nameof(ReadPendingSinceAsync_WithAfterId_SkipsRowsAtOrBeforeTheCompositeCursor));
await using var _w = writer;
var instant = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc);
var sameInstant = Enumerable.Range(0, 5)
.Select(_ => NewEvent(occurredAtUtc: instant))
.ToList();
foreach (var e in sameInstant) await writer.WriteAsync(e);
// First page under the composite order (OccurredAtUtc, EventId).
var page1 = await writer.ReadPendingSinceAsync(instant, batchSize: 2);
Assert.Equal(2, page1.Count);
// Second page continues strictly after the last row of the first — no repeats,
// no stall, even though every row shares the same instant.
var page2 = await writer.ReadPendingSinceAsync(
instant, batchSize: 2, afterId: page1[^1].EventId.ToString());
Assert.Equal(2, page2.Count);
Assert.Empty(page2.Select(r => r.EventId).Intersect(page1.Select(r => r.EventId)));
var page3 = await writer.ReadPendingSinceAsync(
instant, batchSize: 2, afterId: page2[^1].EventId.ToString());
Assert.Single(page3); // the 5th and last
var allIds = page1.Concat(page2).Concat(page3).Select(r => r.EventId).ToHashSet();
Assert.Equal(sameInstant.Select(e => e.EventId).ToHashSet(), allIds);
}
[Fact]
public async Task MarkReconciledUpToAsync_WithCursorId_RetiresOnlyRowsAtOrBeforeIt()
{
var (writer, _) = CreateWriter(nameof(MarkReconciledUpToAsync_WithCursorId_RetiresOnlyRowsAtOrBeforeIt));
await using var _w = writer;
var instant = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc);
var older = NewEvent(occurredAtUtc: instant.AddSeconds(-10));
var atInstant = Enumerable.Range(0, 4).Select(_ => NewEvent(occurredAtUtc: instant)).ToList();
var newer = NewEvent(occurredAtUtc: instant.AddSeconds(10));
await writer.WriteAsync(older);
foreach (var e in atInstant) await writer.WriteAsync(e);
await writer.WriteAsync(newer);
// Central consumed the older row plus the first two at the shared instant.
var consumed = await writer.ReadPendingSinceAsync(instant, batchSize: 2);
var cursorId = consumed[^1].EventId.ToString();
var flipped = await writer.MarkReconciledUpToAsync(instant, cursorId);
// older + the two consumed at the instant.
Assert.Equal(3, flipped);
var remaining = await writer.ReadPendingSinceAsync(DateTime.MinValue, batchSize: 100);
var remainingIds = remaining.Select(r => r.EventId).ToHashSet();
Assert.DoesNotContain(older.EventId, remainingIds);
foreach (var e in consumed) Assert.DoesNotContain(e.EventId, remainingIds);
Assert.Contains(newer.EventId, remainingIds);
Assert.Equal(3, remaining.Count); // the two un-consumed at the instant + newer
}
[Fact]
public async Task MarkReconciledUpToAsync_WithoutCursorId_LeavesTheBoundaryInstantServable()
{
// With a bare timestamp cursor under the inclusive >= read contract, the rows AT
// the cursor instant may be only half-consumed, so they must stay servable. Only
// strictly-older rows are provably received.
var (writer, _) = CreateWriter(nameof(MarkReconciledUpToAsync_WithoutCursorId_LeavesTheBoundaryInstantServable));
await using var _w = writer;
var instant = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc);
var older = NewEvent(occurredAtUtc: instant.AddSeconds(-5));
var boundary = NewEvent(occurredAtUtc: instant);
await writer.WriteAsync(older);
await writer.WriteAsync(boundary);
var flipped = await writer.MarkReconciledUpToAsync(instant, afterId: null);
Assert.Equal(1, flipped);
var remaining = await writer.ReadPendingSinceAsync(DateTime.MinValue, batchSize: 100);
var row = Assert.Single(remaining);
Assert.Equal(boundary.EventId, row.EventId);
}
[Fact]
public async Task ServingRows_WithoutAnAdvancingCursor_KeepsThemServable_AtLeastOnce()
{
// The at-least-once contract end to end at the storage layer: reading a batch
// changes no state, so a central that faults before committing gets the identical
// batch on its next pull. Only an advanced cursor retires rows.
var (writer, _) = CreateWriter(nameof(ServingRows_WithoutAnAdvancingCursor_KeepsThemServable_AtLeastOnce));
await using var _w = writer;
var since = new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc);
var events = Enumerable.Range(1, 3)
.Select(i => NewEvent(occurredAtUtc: since.AddSeconds(i)))
.ToList();
foreach (var e in events) await writer.WriteAsync(e);
var first = await writer.ReadPendingSinceAsync(since, batchSize: 100);
// ... central faults here; its cursor never moves, so it replays the same call.
await writer.MarkReconciledUpToAsync(since, afterId: null);
var second = await writer.ReadPendingSinceAsync(since, batchSize: 100);
Assert.Equal(3, first.Count);
Assert.Equal(
first.Select(r => r.EventId).ToHashSet(),
second.Select(r => r.EventId).ToHashSet());
}
[Fact]
public async Task ReadPendingSinceAsync_InvalidBatchSize_Throws()
{