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:
@@ -1,5 +1,6 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
||||
|
||||
@@ -20,10 +21,15 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
||||
/// Mirrors the sibling <see cref="AuditEventDtoMapper"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Only the DTO→entity direction is provided: nothing in the system maps a
|
||||
/// <see cref="SiteCall"/> back onto the wire (sites emit the operational state
|
||||
/// from <c>SiteCallOperational</c>, never from the central <see cref="SiteCall"/>
|
||||
/// entity), so an entity→DTO method would be dead code.
|
||||
/// Two directions are provided. <see cref="FromDto"/> rehydrates the central
|
||||
/// <see cref="SiteCall"/> entity central writes into the <c>SiteCalls</c> table.
|
||||
/// <see cref="ToDto"/> projects a site-local <see cref="SiteCallOperational"/>
|
||||
/// onto the wire — used by the Site Call Audit (#22) <c>PullSiteCalls</c>
|
||||
/// reconciliation handler (the central→site self-heal pull). The
|
||||
/// <see cref="SiteCall"/> entity itself is never mapped back onto the wire:
|
||||
/// sites emit operational state from <see cref="SiteCallOperational"/>, never
|
||||
/// from the central <see cref="SiteCall"/>, so a <c>SiteCall</c>→DTO method
|
||||
/// would be dead code.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// String nullability convention: proto3 scalar strings cannot be absent, so the
|
||||
@@ -70,4 +76,54 @@ public static class SiteCallDtoMapper
|
||||
IngestedAtUtc = DateTime.UtcNow, // overwritten by AuditLogIngestActor
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projects a site-local <see cref="SiteCallOperational"/> onto its
|
||||
/// wire-format DTO for the Site Call Audit (#22) <c>PullSiteCalls</c>
|
||||
/// reconciliation RPC. The inverse of <see cref="FromDto"/>; null
|
||||
/// <see cref="SiteCallOperational.LastError"/> / <see cref="SiteCallOperational.SourceNode"/>
|
||||
/// collapse to empty strings (proto3 scalar strings cannot be absent), while
|
||||
/// the nullable <c>HttpStatus</c> and <c>TerminalAtUtc</c> stay unset on the
|
||||
/// wire so true-null semantics survive the round-trip back through
|
||||
/// <see cref="FromDto"/>.
|
||||
/// </summary>
|
||||
/// <param name="operational">The site-local operational state to project to wire format.</param>
|
||||
/// <returns>A populated <see cref="SiteCallOperationalDto"/> ready for transmission.</returns>
|
||||
public static SiteCallOperationalDto ToDto(SiteCallOperational operational)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(operational);
|
||||
|
||||
var dto = new SiteCallOperationalDto
|
||||
{
|
||||
TrackedOperationId = operational.TrackedOperationId.ToString(),
|
||||
Channel = operational.Channel,
|
||||
Target = operational.Target,
|
||||
SourceSite = operational.SourceSite,
|
||||
SourceNode = operational.SourceNode ?? string.Empty,
|
||||
Status = operational.Status,
|
||||
RetryCount = operational.RetryCount,
|
||||
LastError = operational.LastError ?? string.Empty,
|
||||
CreatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.CreatedAtUtc)),
|
||||
UpdatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.UpdatedAtUtc)),
|
||||
};
|
||||
|
||||
if (operational.HttpStatus.HasValue)
|
||||
{
|
||||
dto.HttpStatus = operational.HttpStatus.Value;
|
||||
}
|
||||
|
||||
if (operational.TerminalAtUtc.HasValue)
|
||||
{
|
||||
dto.TerminalAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.TerminalAtUtc.Value));
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
// All ScadaBridge timestamps are UTC by invariant; Timestamp.FromDateTime
|
||||
// requires UTC kind. Specify (never convert) so a row read back from SQLite
|
||||
// with Kind=Utc passes through and a defensively-unspecified value is
|
||||
// treated as the UTC it already is. Mirrors AuditEventDtoMapper.EnsureUtc.
|
||||
private static DateTime EnsureUtc(DateTime value) =>
|
||||
value.Kind == DateTimeKind.Utc ? value : DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user