perf(comms): alarms-only seed, capped buffers, at-least-once audit pull
This commit is contained in:
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user