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
@@ -271,6 +271,9 @@ public class SiteAuditTelemetryActor : ReceiveActor
var batch = new CachedTelemetryBatch();
var wireBytes = AuditWireBudget.InitialBytes;
var emittedEventIds = new List<Guid>(pending.Count);
// EventId → the tracking-row version its packet carried, so an accepted
// packet can be recorded as a central ack (review 2026-10-21 06/PERF-2).
var packetVersions = new Dictionary<Guid, (TrackedOperationId Id, DateTime UpdatedAtUtc)>(pending.Count);
// Rows whose operational half can never be built. They are marked
// PushAbandoned so they LEAVE this queue — see AbandonUnresolvableAsync
// for why that is safe and why leaving them Pending is not.
@@ -338,6 +341,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
var packet = BuildCachedPacket(auditRow, snapshot);
if (!AuditWireBudget.TryAppend(batch.Packets, packet, ref wireBytes, auditRow.EventId.ToString())) break;
emittedEventIds.Add(auditRow.EventId);
packetVersions[auditRow.EventId] = (snapshot.Id, snapshot.UpdatedAtUtc);
}
if (lookupFailures > 0)
@@ -389,6 +393,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
{
await _queue.MarkForwardedAsync(acceptedIds, ct)
.ConfigureAwait(false);
await RecordCentralAcksAsync(acceptedIds, packetVersions, ct).ConfigureAwait(false);
}
await RecordRefusalsAsync(
@@ -413,6 +418,51 @@ public class SiteAuditTelemetryActor : ReceiveActor
}
}
/// <summary>
/// Tells the tracking store which tracking-row versions central has just accepted, so
/// the SiteCalls reconciliation pull stops re-serving them (review 2026-10-21
/// 06/PERF-2). Accepted means central committed the packet's <c>SiteCalls</c> upsert
/// in the same transaction as its audit row, i.e. central now holds exactly what a pull
/// of that version would send. Several packets of one operation in a batch collapse to
/// the newest version. Best-effort: the store only enqueues, and a failure here just
/// leaves the rows eligible for one more pull.
/// </summary>
private async Task RecordCentralAcksAsync(
IReadOnlyList<Guid> acceptedIds,
Dictionary<Guid, (TrackedOperationId Id, DateTime UpdatedAtUtc)> packetVersions,
CancellationToken ct)
{
var versions = new Dictionary<TrackedOperationId, DateTime>();
foreach (var eventId in acceptedIds)
{
if (!packetVersions.TryGetValue(eventId, out var v))
{
continue;
}
if (!versions.TryGetValue(v.Id, out var existing) || v.UpdatedAtUtc > existing)
{
versions[v.Id] = v.UpdatedAtUtc;
}
}
if (versions.Count == 0 || _trackingStore is null)
{
return;
}
try
{
await _trackingStore.RecordCentralAcksAsync(versions, ct).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogDebug(ex,
"Recording {Count} central ack(s) in the tracking store failed; those rows stay eligible for the reconciliation pull.",
versions.Count);
}
}
/// <summary>
/// Moves cached rows whose operational half can never be built out of the
/// cached-drain queue, as <see cref="SiteAuditForwardStates.PushAbandoned"/>.