feat(sitecallaudit): PullSiteCalls reconciliation plumbing (store read + RPC + site handler + central client)

Site Call Audit (#22): build the documented periodic reconciliation PULL
self-heal path for the eventually-consistent central SiteCalls mirror, as a
dedicated PullSiteCalls gRPC RPC kept separate from the audit pull. This is the
pull PLUMBING only; the central reconciliation tick is a separate follow-up.

- IOperationTrackingStore.ReadChangedSinceAsync(sinceUtc, batchSize): inclusive
  UpdatedAtUtc cursor, oldest-first, batch-capped; SQLite impl projects tracking
  rows onto SiteCallOperational (Kind->Channel, TargetSummary->Target, SourceSite
  left empty - the store has no site-id column).
- sitestream.proto: rpc PullSiteCalls + PullSiteCallsRequest/Response, mirroring
  PullAuditEvents; regenerated checked-in SiteStreamGrpc/*.cs.
- SiteCallDtoMapper.ToDto(SiteCallOperational): inverse of FromDto for the handler.
- SiteStreamGrpcServer.PullSiteCalls handler + SetOperationTrackingStore seam;
  Host wires the seam alongside SetSiteAuditQueue (site roles only).
- Central IPullSiteCallsClient + GrpcPullSiteCallsClient (home: AuditLog/Central to
  reuse ISiteEnumerator; SiteCallAudit does not reference AuditLog). Re-stamps
  SourceSite from the dialed siteId; no-throw on tolerable transport faults;
  SpecifyKind (not ToUniversalTime) cursor handling. Central-only DI registration.

Tests: ReadChangedSinceAsync (4), PullSiteCalls handler (6), GrpcPullSiteCallsClient
(8). Full solution build 0 warnings/0 errors (TreatWarningsAsErrors).
This commit is contained in:
Joseph Doherty
2026-06-15 10:39:06 -04:00
parent c092e89fd1
commit 963e3427da
15 changed files with 1751 additions and 23 deletions
@@ -0,0 +1,57 @@
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Mockable abstraction over the central-side <c>PullSiteCalls</c> gRPC client
/// surface used by the Site Call Audit (#22) reconciliation tick to fetch the
/// next batch of cached-call operational rows from a specific site — the
/// documented periodic self-heal pull that backfills the eventually-consistent
/// central <c>SiteCalls</c> mirror when best-effort push telemetry is lost.
/// Extracted so the (separate, follow-up) reconciliation actor can be
/// unit-tested against an in-memory stub without standing up a real
/// <c>GrpcChannel</c> per site.
/// </summary>
/// <remarks>
/// <para>
/// The home is <c>ZB.MOM.WW.ScadaBridge.AuditLog.Central</c> rather than the
/// <c>ZB.MOM.WW.ScadaBridge.SiteCallAudit</c> project so it can reuse the
/// <see cref="ISiteEnumerator"/> / <see cref="SiteEntry"/> endpoint-resolution
/// abstraction that already lives here (and that the sibling
/// <see cref="IPullAuditEventsClient"/> uses) — SiteCallAudit does not reference
/// AuditLog, so hosting the client there would mean duplicating the enumerator.
/// This mirrors the decision to keep <see cref="SiteCallDtoMapper"/> in
/// <c>ZB.MOM.WW.ScadaBridge.Communication</c>.
/// </para>
/// <para>
/// Implementations MUST NOT throw on transport faults the reconciliation tick
/// can tolerate (connection refused, deadline exceeded, cancellation) — one
/// offline site must never sink the rest of the tick. The
/// <see cref="PullSiteCallsResponse.SiteCalls"/> are returned oldest-first by
/// <c>UpdatedAtUtc</c> with the <c>SourceSite</c> re-stamped from the dialed
/// site id (the site leaves it empty, being unaware of its own id), and a
/// <c>MoreAvailable</c> flag the caller uses to decide whether to fire another
/// pull immediately.
/// </para>
/// </remarks>
public interface IPullSiteCallsClient
{
/// <summary>
/// Issues a <c>PullSiteCalls</c> RPC against the site whose gRPC endpoint is
/// registered against <paramref name="siteId"/>. Returns the next batch of
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.SiteCall"/> rows
/// ordered oldest-first (with <c>SourceSite</c> re-stamped from
/// <paramref name="siteId"/>) AND a <c>MoreAvailable</c> flag the caller uses
/// to decide whether to fire another pull immediately.
/// </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="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,
int batchSize,
CancellationToken ct);
}