fix(site-call-audit): composite keyset cursor eliminates the single-timestamp reconciliation pin; pinned state now a published event

Reconciliation cursor becomes composite (UpdatedAtUtc, TrackedOperationId);
IPullSiteCallsClient/GrpcPullSiteCallsClient forward the Task-15 after_id keyset
(additive param, null preserves the legacy inclusive >= contract). A burst
sharing one exact UpdatedAtUtc now drains via the id tiebreak instead of pinning
forever. A legacy site that ignores after_id is latched + published as
SiteCallReconciliationPinnedChanged on the EventStream (transition-only),
replacing the prior silent log line.
This commit is contained in:
Joseph Doherty
2026-07-09 08:44:07 -04:00
parent 1f4c0b67ca
commit 20098c6108
9 changed files with 324 additions and 66 deletions
@@ -80,6 +80,7 @@ public sealed class GrpcPullSiteCallsClient : IPullSiteCallsClient
public async Task<PullSiteCallsResponse> PullAsync(
string siteId,
DateTime sinceUtc,
string? afterId,
int batchSize,
CancellationToken ct)
{
@@ -100,6 +101,10 @@ public sealed class GrpcPullSiteCallsClient : IPullSiteCallsClient
// EnsureUtc keeps Timestamp.FromDateTime happy (it requires UTC kind).
SinceUtc = Timestamp.FromDateTime(EnsureUtc(sinceUtc)),
BatchSize = batchSize,
// Composite-keyset tiebreak (Task 16). proto3 has no nullable string —
// an unset/empty AfterId is the site's signal to keep the legacy
// inclusive-timestamp contract (also what a first pull sends).
AfterId = afterId ?? string.Empty,
};
ProtoPullResponse reply;
@@ -46,12 +46,22 @@ public interface IPullSiteCallsClient
/// </summary>
/// <param name="siteId">The identifier of the site to pull cached-call operational rows from.</param>
/// <param name="sinceUtc">Only rows with an <c>UpdatedAtUtc</c> at or after this cursor time are returned.</param>
/// <param name="afterId">
/// The composite-keyset tiebreak cursor (Task 16). When non-null it is the
/// <c>TrackedOperationId</c> of the last row already consumed at
/// <paramref name="sinceUtc"/>; the site returns only rows strictly greater
/// than the composite <c>(UpdatedAtUtc, TrackedOperationId)</c> pair, so a
/// burst sharing one exact <c>UpdatedAtUtc</c> drains via the id tiebreak
/// instead of pinning the inclusive-timestamp cursor. Null on the first pull
/// (or against a legacy site) preserves the inclusive <c>&gt;=</c> contract.
/// </param>
/// <param name="batchSize">Maximum number of rows to return per call.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the next reconciliation batch with a <c>MoreAvailable</c> flag.</returns>
Task<PullSiteCallsResponse> PullAsync(
string siteId,
DateTime sinceUtc,
string? afterId,
int batchSize,
CancellationToken ct);
}