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:
@@ -5,7 +5,9 @@ using Grpc.Core;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Observability;
|
||||
using GrpcStatus = Grpc.Core.Status;
|
||||
@@ -48,6 +50,14 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
|
||||
// the missing queue as "nothing to ship" and returns an empty response so
|
||||
// central retries on its next reconciliation cycle.
|
||||
private ISiteAuditQueue? _siteAuditQueue;
|
||||
// Site Call Audit (#22): site-local operation-tracking store handed in by
|
||||
// AkkaHostedService on site roles so the central reconciliation puller's
|
||||
// PullSiteCalls RPC can read tracking rows changed since a cursor. Null
|
||||
// when not wired (central-only host or test composing the server in
|
||||
// isolation) — the handler treats the missing store as "nothing to ship"
|
||||
// and returns an empty response so central retries on its next cycle.
|
||||
// Mirrors _siteAuditQueue.
|
||||
private IOperationTrackingStore? _operationTrackingStore;
|
||||
|
||||
/// <summary>
|
||||
/// Test-only constructor — kept <c>internal</c> so the DI container sees a
|
||||
@@ -137,6 +147,21 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
|
||||
_siteAuditQueue = queue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hands the site-local <see cref="IOperationTrackingStore"/> (the same
|
||||
/// <c>OperationTrackingStore</c> singleton that backs
|
||||
/// <c>Tracking.Status(id)</c> on the script thread) to the gRPC server so
|
||||
/// the Site Call Audit (#22) <see cref="PullSiteCalls"/> RPC can serve
|
||||
/// central's reconciliation pulls. Mirrors <see cref="SetSiteAuditQueue"/>:
|
||||
/// wired post-construction because the store and the gRPC server are both
|
||||
/// DI singletons brought up in independent orders on site startup.
|
||||
/// </summary>
|
||||
/// <param name="store">The site operation-tracking store for serving reconciliation pulls.</param>
|
||||
public void SetOperationTrackingStore(IOperationTrackingStore store)
|
||||
{
|
||||
_operationTrackingStore = store;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Host-017 / REQ-HOST-7: signals the gRPC server to begin its part of the
|
||||
/// site shutdown sequence — refuse new <see cref="SubscribeInstance"/>
|
||||
@@ -488,6 +513,69 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<PullSiteCallsResponse> PullSiteCalls(
|
||||
PullSiteCallsRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var store = _operationTrackingStore;
|
||||
if (store is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"PullSiteCalls invoked before SetOperationTrackingStore was called; returning empty response.");
|
||||
return new PullSiteCallsResponse();
|
||||
}
|
||||
|
||||
if (request.BatchSize <= 0)
|
||||
{
|
||||
// Mirrors PullAuditEvents: reject malformed requests cleanly with
|
||||
// InvalidArgument so the caller doesn't see a generic RpcException
|
||||
// from the underlying SQLite parameter validation.
|
||||
throw new RpcException(new GrpcStatus(
|
||||
StatusCode.InvalidArgument, "batch_size must be > 0"));
|
||||
}
|
||||
|
||||
// since_utc defaults to DateTime.MinValue when the wrapper is absent —
|
||||
// i.e. "pull from the beginning of recorded history", the intended
|
||||
// behaviour for the very first reconciliation cycle. ToUniversalTime
|
||||
// is safe here (the wire value is always a real UTC Timestamp, never the
|
||||
// unspecified-MinValue the central client guards against on its side).
|
||||
var since = request.SinceUtc?.ToDateTime().ToUniversalTime() ?? DateTime.MinValue;
|
||||
|
||||
IReadOnlyList<SiteCallOperational> operationals;
|
||||
try
|
||||
{
|
||||
operationals = await store.ReadChangedSinceAsync(
|
||||
since, request.BatchSize, context.CancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Best-effort, like PullAuditEvents: a read fault must never abort
|
||||
// the reconciliation tick — central retries on its next cycle.
|
||||
_logger.LogError(ex,
|
||||
"ReadChangedSinceAsync failed for since={Since} batch={Batch}; returning empty response.",
|
||||
since, request.BatchSize);
|
||||
return new PullSiteCallsResponse();
|
||||
}
|
||||
|
||||
var response = new PullSiteCallsResponse
|
||||
{
|
||||
// batch_size saturated → tell central to issue a follow-up pull with
|
||||
// an advanced cursor. The site doesn't compute the cursor — central
|
||||
// walks it forward from the last returned UpdatedAtUtc. Unlike
|
||||
// PullAuditEvents there is no MarkReconciled step: the tracking store
|
||||
// is the operational source of truth and the central SiteCalls mirror
|
||||
// is upsert-on-newer, so re-reading rows is a harmless no-op.
|
||||
MoreAvailable = operationals.Count >= request.BatchSize,
|
||||
};
|
||||
foreach (var op in operationals)
|
||||
{
|
||||
response.Operationals.Add(SiteCallDtoMapper.ToDto(op));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks a single active stream so cleanup only removes its own entry.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user