feat(site-call-audit): additive (UpdatedAtUtc, TrackedOperationId) keyset in the reconciliation pull contract

Site side: additive proto after_id field, ReadChangedSinceAsync gains an optional
afterId cursor and deterministic (UpdatedAtUtc, TrackedOperationId) ordering — a
batch fully inside one UpdatedAtUtc instant no longer re-reads the same page
forever. Absent/empty afterId preserves the exact legacy inclusive >= contract, so
an older central is unaffected. Regenerated the checked-in gRPC C#.
This commit is contained in:
Joseph Doherty
2026-07-09 08:31:42 -04:00
parent 2c45c3238b
commit a608d7a79b
7 changed files with 285 additions and 43 deletions
@@ -137,6 +137,19 @@ public interface IOperationTrackingStore
/// the caller advance the cursor monotonically across follow-up pulls.
/// </para>
/// <para>
/// <b>Composite keyset (Task 15).</b> When <paramref name="afterId"/> is
/// supplied the read uses a <c>(UpdatedAtUtc, TrackedOperationId)</c> keyset:
/// it returns rows strictly after that cursor — <c>UpdatedAtUtc &gt; sinceUtc</c>,
/// or <c>UpdatedAtUtc = sinceUtc</c> with a <c>TrackedOperationId</c> greater
/// than <paramref name="afterId"/>. This un-pins a batch that would otherwise
/// stall: when more than <paramref name="batchSize"/> rows share one
/// <c>UpdatedAtUtc</c>, the plain inclusive <c>&gt;=</c> resume re-reads the
/// same page forever. When <paramref name="afterId"/> is <c>null</c>/empty the
/// legacy inclusive <c>&gt;=</c> contract is preserved exactly (an older
/// central that does not send the cursor is unaffected). Ordering is always
/// deterministic — <c>UpdatedAtUtc ASC, TrackedOperationId ASC</c>.
/// </para>
/// <para>
/// <see cref="SiteCallOperational.SourceSite"/> is left as the empty string:
/// the site id is not a tracking-store column, and the central client re-stamps
/// it from the <c>siteId</c> it dialed (the only authority that knows which
@@ -148,10 +161,17 @@ public interface IOperationTrackingStore
/// </remarks>
/// <param name="sinceUtc">Inclusive lower bound on <c>UpdatedAtUtc</c>; <see cref="DateTime.MinValue"/> reads from the start.</param>
/// <param name="batchSize">Maximum number of rows to return (oldest first).</param>
/// <param name="afterId">
/// Optional composite-keyset cursor — the <c>TrackedOperationId</c> of the last
/// row already consumed at <paramref name="sinceUtc"/>. When supplied the read
/// returns only rows strictly after <c>(sinceUtc, afterId)</c>; when
/// <c>null</c>/empty the legacy inclusive <c>&gt;= sinceUtc</c> behaviour applies.
/// </param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The matching rows projected to <see cref="SiteCallOperational"/>, oldest-first, capped at <paramref name="batchSize"/>.</returns>
Task<IReadOnlyList<SiteCallOperational>> ReadChangedSinceAsync(
DateTime sinceUtc,
int batchSize,
string? afterId = null,
CancellationToken ct = default);
}