fix(site-call-audit): reconciliation pull serves only un-acked tracking rows (node-local ack ledger), batched page upsert, prune removed sites' cursors (review 2026-10-21 06/PERF-2 + lane H follow-up)

This commit is contained in:
Joseph Doherty
2026-10-22 09:35:30 -04:00
parent e69be1654f
commit 88273693f9
12 changed files with 789 additions and 10 deletions
@@ -1057,4 +1057,124 @@ public class OperationTrackingStoreTests : IDisposable
Assert.Equal(System.Data.ConnectionState.Closed, connection.State);
}
// ── Central-ack filter for the reconciliation pull (review 2026-10-21 06/PERF-2) ──
/// <summary>
/// Verifies that a row whose current version central has acknowledged through the
/// cached-telemetry push is no longer served by the reconciliation read, while
/// unacknowledged rows still are.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadChangedSince_SkipsRowsCentralAckedAtTheirCurrentVersion()
{
var (store, _) = CreateStore(nameof(ReadChangedSince_SkipsRowsCentralAckedAtTheirCurrentVersion));
await using var _store = store;
var acked = TrackedOperationId.New();
var unacked = TrackedOperationId.New();
await store.RecordEnqueueAsync(acked, nameof(AuditKind.ApiCallCached), "A", null, null, "node-a");
await store.RecordEnqueueAsync(unacked, nameof(AuditKind.ApiCallCached), "B", null, null, "node-a");
var snapshot = await store.GetStatusAsync(acked);
await store.RecordCentralAcksAsync(new Dictionary<TrackedOperationId, DateTime>
{
[acked] = snapshot!.UpdatedAtUtc,
});
var rows = await store.ReadChangedSinceAsync(DateTime.MinValue, batchSize: 100);
Assert.Equal(new[] { unacked }, rows.Select(r => r.TrackedOperationId).ToArray());
}
/// <summary>
/// Verifies that a row changed AFTER central acknowledged it is served again — the ack
/// covers one version, not the operation.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadChangedSince_ServesRowChangedAfterItsAck()
{
var (store, _) = CreateStore(nameof(ReadChangedSince_ServesRowChangedAfterItsAck));
await using var _store = store;
var id = TrackedOperationId.New();
await store.RecordEnqueueAsync(id, nameof(AuditKind.ApiCallCached), "A", null, null, "node-a");
var submitted = await store.GetStatusAsync(id);
await store.RecordCentralAcksAsync(new Dictionary<TrackedOperationId, DateTime>
{
[id] = submitted!.UpdatedAtUtc,
});
await Task.Delay(5); // a strictly later UpdatedAtUtc stamp
await store.RecordTerminalAsync(id, nameof(AuditStatus.Delivered), null, 200);
var rows = await store.ReadChangedSinceAsync(DateTime.MinValue, batchSize: 100);
var row = Assert.Single(rows);
Assert.Equal(nameof(AuditStatus.Delivered), row.Status);
}
/// <summary>
/// Verifies that an older ack never rolls back a newer one (acks are monotonic per row),
/// and that the ack ledger is pruned with the tracking rows it describes.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CentralAcks_AreMonotonic_AndPrunedWithTheirTrackingRow()
{
var (store, dataSource) = CreateStore(nameof(CentralAcks_AreMonotonic_AndPrunedWithTheirTrackingRow));
await using var _store = store;
var id = TrackedOperationId.New();
await store.RecordEnqueueAsync(id, nameof(AuditKind.ApiCallCached), "A", null, null, "node-a");
await store.RecordTerminalAsync(id, nameof(AuditStatus.Delivered), null, 200);
var terminal = await store.GetStatusAsync(id);
await store.RecordCentralAcksAsync(new Dictionary<TrackedOperationId, DateTime> { [id] = terminal!.UpdatedAtUtc });
await store.RecordCentralAcksAsync(new Dictionary<TrackedOperationId, DateTime> { [id] = terminal.CreatedAtUtc.AddSeconds(-1) });
Assert.Empty(await store.ReadChangedSinceAsync(DateTime.MinValue, batchSize: 100));
// Age the terminal row past retention and purge: the ack row goes with it.
await store.FlushAsync();
using (var connection = OpenVerifierConnection(dataSource))
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "UPDATE OperationTracking SET TerminalAtUtc = $old;";
cmd.Parameters.AddWithValue("$old", DateTime.UtcNow.AddDays(-30).ToString("o"));
cmd.ExecuteNonQuery();
}
await store.PurgeTerminalAsync(DateTime.UtcNow.AddDays(-7));
using (var connection = OpenVerifierConnection(dataSource))
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "SELECT COUNT(*) FROM OperationTrackingCentralAck;";
Assert.Equal(0L, (long)cmd.ExecuteScalar()!);
}
}
/// <summary>
/// Verifies the ack ledger is its own table, not a column on the LocalDb-replicated
/// <c>OperationTracking</c> table (whose capture triggers are built from its column list).
/// Being node-local, a standby that takes over has no ack rows and serves every row once.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CentralAckLedger_IsADistinctNodeLocalTable_NotAColumnOnTheReplicatedTable()
{
var (store, dataSource) = CreateStore(nameof(CentralAckLedger_IsADistinctNodeLocalTable_NotAColumnOnTheReplicatedTable));
await using var _store = store;
await store.FlushAsync();
using var connection = OpenVerifierConnection(dataSource);
using var cmd = connection.CreateCommand();
cmd.CommandText = "PRAGMA table_info(OperationTracking);";
var columns = new List<string>();
using (var reader = cmd.ExecuteReader())
{
while (reader.Read()) columns.Add(reader.GetString(1));
}
Assert.DoesNotContain(columns, c => c.StartsWith("Acked", StringComparison.Ordinal)
|| c.Contains("CentralAck", StringComparison.Ordinal));
cmd.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'OperationTrackingCentralAck';";
Assert.Equal(1L, (long)cmd.ExecuteScalar()!);
}
}