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:
@@ -0,0 +1,287 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
||||
using ProtoPullRequest = ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest;
|
||||
using ProtoPullResponse = ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse;
|
||||
using PullSiteCallsResponse = ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration.PullSiteCallsResponse;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
||||
|
||||
/// <summary>
|
||||
/// Production <see cref="IPullSiteCallsClient"/> (Site Call Audit #22) that the
|
||||
/// central reconciliation tick (a separate follow-up component) uses to pull the
|
||||
/// next batch of cached-call operational rows from a site over the
|
||||
/// <c>PullSiteCalls</c> unary gRPC RPC served by <c>SiteStreamGrpcServer</c>.
|
||||
/// A near-exact sibling of <see cref="GrpcPullAuditEventsClient"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Endpoint resolution.</b> The caller passes only a <c>siteId</c>; this
|
||||
/// client resolves it to a gRPC authority via <see cref="ISiteEnumerator"/>
|
||||
/// (<see cref="SiteEntry.GrpcEndpoint"/>) on every call so a NodeA→NodeB
|
||||
/// failover flip or an edited site address takes effect on the next tick. A site
|
||||
/// with no registered endpoint yields an empty response (no dial).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>SourceSite re-stamp.</b> The site leaves
|
||||
/// <c>SiteCallOperationalDto.SourceSite</c> empty (the tracking store has no
|
||||
/// site-id column). This client is the authority that knows which site it
|
||||
/// dialed, so it re-stamps the mapped <see cref="SiteCall.SourceSite"/> from
|
||||
/// <c>siteId</c> — the same "re-stamp from the forwarder's own id" pattern the
|
||||
/// site push path uses.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Fault tolerance.</b> Per the <see cref="IPullSiteCallsClient"/> contract,
|
||||
/// tolerable transport faults (<see cref="StatusCode.Unavailable"/>,
|
||||
/// <see cref="StatusCode.DeadlineExceeded"/>, <see cref="StatusCode.Cancelled"/>,
|
||||
/// bare <see cref="HttpRequestException"/> / <c>SocketException</c>) are caught
|
||||
/// and collapsed to an empty response so one offline site never sinks the rest
|
||||
/// of the reconciliation tick. Any other fault (e.g. a malformed reply that
|
||||
/// fails DTO mapping) is also swallowed to empty: reconciliation is best-effort.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Testability.</b> The unary call is reached through the
|
||||
/// <see cref="IPullSiteCallsInvoker"/> seam. Production binds
|
||||
/// <see cref="GrpcPullSiteCallsInvoker"/> (one cached <see cref="GrpcChannel"/>
|
||||
/// per endpoint, keepalive from <see cref="CommunicationOptions"/>); unit tests
|
||||
/// inject a fake invoker so no real HTTP/2 endpoint is required.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class GrpcPullSiteCallsClient : IPullSiteCallsClient
|
||||
{
|
||||
private readonly ISiteEnumerator _sites;
|
||||
private readonly IPullSiteCallsInvoker _invoker;
|
||||
private readonly ILogger<GrpcPullSiteCallsClient> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the client over the given site enumerator and unary-call invoker.
|
||||
/// </summary>
|
||||
/// <param name="sites">Resolves a <c>siteId</c> to its gRPC endpoint.</param>
|
||||
/// <param name="invoker">Seam that issues the <c>PullSiteCalls</c> unary RPC against a resolved endpoint.</param>
|
||||
/// <param name="logger">Logger for transport-fault diagnostics.</param>
|
||||
public GrpcPullSiteCallsClient(
|
||||
ISiteEnumerator sites,
|
||||
IPullSiteCallsInvoker invoker,
|
||||
ILogger<GrpcPullSiteCallsClient> logger)
|
||||
{
|
||||
_sites = sites ?? throw new ArgumentNullException(nameof(sites));
|
||||
_invoker = invoker ?? throw new ArgumentNullException(nameof(invoker));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PullSiteCallsResponse> PullAsync(
|
||||
string siteId,
|
||||
DateTime sinceUtc,
|
||||
int batchSize,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var endpoint = await ResolveEndpointAsync(siteId, ct).ConfigureAwait(false);
|
||||
if (endpoint is null)
|
||||
{
|
||||
// No gRPC address registered for the site — a configuration decision
|
||||
// (mirrors ISiteEnumerator's own contract), not a runtime error, so
|
||||
// there is simply nothing to pull.
|
||||
_logger.LogDebug(
|
||||
"PullSiteCalls skipped: no gRPC endpoint registered for site {SiteId}.", siteId);
|
||||
return Empty;
|
||||
}
|
||||
|
||||
var request = new ProtoPullRequest
|
||||
{
|
||||
// ReadChangedSinceAsync treats DateTime.MinValue as "from the start";
|
||||
// EnsureUtc keeps Timestamp.FromDateTime happy (it requires UTC kind).
|
||||
SinceUtc = Timestamp.FromDateTime(EnsureUtc(sinceUtc)),
|
||||
BatchSize = batchSize,
|
||||
};
|
||||
|
||||
ProtoPullResponse reply;
|
||||
try
|
||||
{
|
||||
reply = await _invoker.InvokeAsync(endpoint, request, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (RpcException ex) when (IsTolerable(ex.StatusCode))
|
||||
{
|
||||
_logger.LogDebug(ex,
|
||||
"PullSiteCalls tolerable transport fault for site {SiteId} ({Endpoint}): {Status}. Returning empty batch.",
|
||||
siteId, endpoint, ex.StatusCode);
|
||||
return Empty;
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or System.Net.Sockets.SocketException)
|
||||
{
|
||||
_logger.LogDebug(ex,
|
||||
"PullSiteCalls connection-layer fault for site {SiteId} ({Endpoint}). Returning empty batch.",
|
||||
siteId, endpoint);
|
||||
return Empty;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Reconciliation tick cancelled — caller token (host shutdown) or an
|
||||
// internal gRPC deadline / linked-CTS cancellation. Both tolerable for
|
||||
// a best-effort pull; collapse to empty rather than landing noisily in
|
||||
// the catch-all below.
|
||||
return Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Any other fault. Reconciliation is best-effort; swallow to empty
|
||||
// rather than throw — the (future) actor's per-site guard would only
|
||||
// re-catch it.
|
||||
_logger.LogWarning(ex,
|
||||
"PullSiteCalls unexpected fault for site {SiteId} ({Endpoint}). Returning empty batch.",
|
||||
siteId, endpoint);
|
||||
return Empty;
|
||||
}
|
||||
|
||||
// Map proto DTOs to central SiteCall entities, re-stamp SourceSite from
|
||||
// the dialed siteId (the site leaves it empty), and order oldest-first by
|
||||
// UpdatedAtUtc (the wire is already ordered by the site read, but the
|
||||
// contract is explicit, so sort defensively).
|
||||
var siteCalls = reply.Operationals
|
||||
.Select(SiteCallDtoMapper.FromDto)
|
||||
.Select(sc => sc with { SourceSite = siteId })
|
||||
.OrderBy(sc => sc.UpdatedAtUtc)
|
||||
.ToList();
|
||||
|
||||
return new PullSiteCallsResponse(siteCalls, reply.MoreAvailable);
|
||||
}
|
||||
|
||||
private async Task<string?> ResolveEndpointAsync(string siteId, CancellationToken ct)
|
||||
{
|
||||
var sites = await _sites.EnumerateAsync(ct).ConfigureAwait(false);
|
||||
foreach (var site in sites)
|
||||
{
|
||||
if (string.Equals(site.SiteId, siteId, StringComparison.Ordinal) &&
|
||||
!string.IsNullOrWhiteSpace(site.GrpcEndpoint))
|
||||
{
|
||||
return site.GrpcEndpoint;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readonly PullSiteCallsResponse Empty =
|
||||
new(Array.Empty<SiteCall>(), MoreAvailable: false);
|
||||
|
||||
private static bool IsTolerable(StatusCode code) => code is
|
||||
StatusCode.Unavailable or
|
||||
StatusCode.DeadlineExceeded or
|
||||
StatusCode.Cancelled;
|
||||
|
||||
// All ScadaBridge timestamps are UTC by invariant. A non-UTC cursor (the
|
||||
// reconciliation cursor starts at DateTime.MinValue, Kind=Unspecified) is
|
||||
// treated AS UTC — never ToUniversalTime()-converted: on a host with a
|
||||
// positive UTC offset MinValue.ToUniversalTime() underflows and
|
||||
// Timestamp.FromDateTime throws, crashing the first pull for every site.
|
||||
private static DateTime EnsureUtc(DateTime value) =>
|
||||
value.Kind == DateTimeKind.Utc ? value : DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>
|
||||
/// Seam over the <c>PullSiteCalls</c> unary gRPC call against a resolved site
|
||||
/// endpoint. Extracted so <see cref="GrpcPullSiteCallsClient"/> can be
|
||||
/// unit-tested without a real <see cref="GrpcChannel"/>. Production binds
|
||||
/// <see cref="GrpcPullSiteCallsInvoker"/>.
|
||||
/// </summary>
|
||||
public interface IPullSiteCallsInvoker
|
||||
{
|
||||
/// <summary>
|
||||
/// Issues the <c>PullSiteCalls</c> unary RPC against <paramref name="endpoint"/>.
|
||||
/// May throw <see cref="RpcException"/> / <see cref="HttpRequestException"/>
|
||||
/// on transport faults — the caller classifies and swallows tolerable ones.
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The site gRPC authority (e.g. <c>http://site-a:8083</c>).</param>
|
||||
/// <param name="request">The wire-format pull request.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The wire-format pull response.</returns>
|
||||
Task<ProtoPullResponse> InvokeAsync(string endpoint, ProtoPullRequest request, CancellationToken ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production <see cref="GrpcPullSiteCallsClient.IPullSiteCallsInvoker"/>: caches
|
||||
/// one <see cref="GrpcChannel"/> per endpoint (keepalive from
|
||||
/// <see cref="CommunicationOptions"/>, mirroring <c>SiteStreamGrpcClient</c>) and
|
||||
/// issues the unary <c>PullSiteCallsAsync</c> call. The cache is keyed by
|
||||
/// endpoint string, so a changed site address (NodeA→NodeB failover flip / an
|
||||
/// edited gRPC address) is reached as soon as the resolver hands the new endpoint
|
||||
/// to <see cref="InvokeAsync"/>. The channel for a previous address lingers idle
|
||||
/// until <see cref="Dispose"/> (idle channels hold no streams — a minor cache
|
||||
/// footprint cost, not a correctness or liveness gap). Sibling of
|
||||
/// <see cref="GrpcPullAuditEventsInvoker"/>.
|
||||
/// </summary>
|
||||
public sealed class GrpcPullSiteCallsInvoker
|
||||
: GrpcPullSiteCallsClient.IPullSiteCallsInvoker, IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, GrpcChannel> _channels = new(StringComparer.Ordinal);
|
||||
private readonly CommunicationOptions _options;
|
||||
|
||||
/// <summary>Creates the invoker using default <see cref="CommunicationOptions"/>.</summary>
|
||||
public GrpcPullSiteCallsInvoker()
|
||||
: this(new CommunicationOptions())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the invoker, applying the configured gRPC keepalive settings to
|
||||
/// every channel it opens.
|
||||
/// </summary>
|
||||
/// <param name="options">Communication options supplying gRPC keepalive timings.</param>
|
||||
public GrpcPullSiteCallsInvoker(CommunicationOptions options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ProtoPullResponse> InvokeAsync(
|
||||
string endpoint, ProtoPullRequest request, CancellationToken ct)
|
||||
{
|
||||
var channel = GetOrCreateChannel(endpoint);
|
||||
var client = new SiteStreamService.SiteStreamServiceClient(channel);
|
||||
using var call = client.PullSiteCallsAsync(request, cancellationToken: ct);
|
||||
return await call.ResponseAsync.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Race-safe channel cache (create-then-GetOrAdd-then-dispose-if-lost): two
|
||||
// concurrent first dials of the same endpoint can both build a GrpcChannel;
|
||||
// only the channel actually installed survives, the loser is disposed.
|
||||
// Mirrors SiteStreamGrpcClientFactory / GrpcPullAuditEventsInvoker.
|
||||
private GrpcChannel GetOrCreateChannel(string endpoint)
|
||||
{
|
||||
if (!_channels.TryGetValue(endpoint, out var channel))
|
||||
{
|
||||
var created = CreateChannel(endpoint);
|
||||
channel = _channels.GetOrAdd(endpoint, created);
|
||||
if (!ReferenceEquals(channel, created))
|
||||
{
|
||||
created.Dispose();
|
||||
}
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
private GrpcChannel CreateChannel(string endpoint) =>
|
||||
GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions
|
||||
{
|
||||
HttpHandler = new SocketsHttpHandler
|
||||
{
|
||||
KeepAlivePingDelay = _options.GrpcKeepAlivePingDelay,
|
||||
KeepAlivePingTimeout = _options.GrpcKeepAlivePingTimeout,
|
||||
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
|
||||
},
|
||||
});
|
||||
|
||||
/// <summary>Disposes all cached channels.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var channel in _channels.Values)
|
||||
{
|
||||
channel.Dispose();
|
||||
}
|
||||
_channels.Clear();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -473,6 +473,31 @@ public static class ServiceCollectionExtensions
|
||||
sp.GetRequiredService<GrpcPullAuditEventsClient.IPullAuditEventsInvoker>(),
|
||||
sp.GetRequiredService<ILogger<GrpcPullAuditEventsClient>>()));
|
||||
|
||||
// Site Call Audit (#22) reconciliation pull client — central-only, the
|
||||
// sibling of the audit pull client above. Lives here (not in the
|
||||
// SiteCallAudit project) so it can reuse the central-only
|
||||
// ISiteEnumerator registered just above; SiteCallAudit does not
|
||||
// reference AuditLog. The invoker owns the per-endpoint GrpcChannel
|
||||
// cache, so it must be a singleton (a fresh invoker per resolution
|
||||
// would leak channels). CommunicationOptions flow through when bound by
|
||||
// the central Host, else defaults — mirrors the audit invoker.
|
||||
services.TryAddSingleton<GrpcPullSiteCallsInvoker>(sp =>
|
||||
{
|
||||
var options = sp
|
||||
.GetService<Microsoft.Extensions.Options.IOptions<
|
||||
ZB.MOM.WW.ScadaBridge.Communication.CommunicationOptions>>();
|
||||
return options is null
|
||||
? new GrpcPullSiteCallsInvoker()
|
||||
: new GrpcPullSiteCallsInvoker(options.Value);
|
||||
});
|
||||
services.TryAddSingleton<GrpcPullSiteCallsClient.IPullSiteCallsInvoker>(
|
||||
sp => sp.GetRequiredService<GrpcPullSiteCallsInvoker>());
|
||||
|
||||
services.TryAddSingleton<IPullSiteCallsClient>(sp => new GrpcPullSiteCallsClient(
|
||||
sp.GetRequiredService<ISiteEnumerator>(),
|
||||
sp.GetRequiredService<GrpcPullSiteCallsClient.IPullSiteCallsInvoker>(),
|
||||
sp.GetRequiredService<ILogger<GrpcPullSiteCallsClient>>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,4 +118,40 @@ public interface IOperationTrackingStore
|
||||
Task PurgeTerminalAsync(
|
||||
DateTime olderThanUtc,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reconciliation read (Site Call Audit #22): return tracking rows whose
|
||||
/// <c>UpdatedAtUtc</c> is at or after <paramref name="sinceUtc"/> as
|
||||
/// <see cref="SiteCallOperational"/> projections, ordered by
|
||||
/// <c>UpdatedAtUtc</c> ascending and capped at <paramref name="batchSize"/>.
|
||||
/// This is the site-side feed for central's <c>PullSiteCalls</c> RPC — the
|
||||
/// documented periodic self-heal pull that backfills the eventually-consistent
|
||||
/// central <c>SiteCalls</c> mirror when best-effort push telemetry is lost.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The lower bound is inclusive so a caller can resume from the last
|
||||
/// returned <c>UpdatedAtUtc</c> without skipping a row that shares that
|
||||
/// instant; central ingest is insert-if-not-exists then upsert-on-newer, so
|
||||
/// re-reading the boundary row is a harmless no-op. The oldest-first cap lets
|
||||
/// the caller advance the cursor monotonically across follow-up pulls.
|
||||
/// </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
|
||||
/// site the rows came from). <see cref="SiteCallOperational.Channel"/> is
|
||||
/// projected from the row's <c>Kind</c> (<c>DbWriteCached → DbOutbound</c>,
|
||||
/// otherwise <c>ApiOutbound</c>) and <see cref="SiteCallOperational.Target"/>
|
||||
/// from <c>TargetSummary</c>.
|
||||
/// </para>
|
||||
/// </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="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,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Site Call Audit (#22) periodic reconciliation pull response: the next batch of
|
||||
/// site cached-call operational rows (the eventually-consistent <c>SiteCalls</c>
|
||||
/// mirror's self-heal feed) plus a <paramref name="MoreAvailable"/> flag signalling
|
||||
/// the caller to advance the watermark and pull again. Mirrors
|
||||
/// <see cref="PullAuditEventsResponse"/>; carries the central <see cref="SiteCall"/>
|
||||
/// entity the ingest path upserts. See Component-SiteCallAudit.md.
|
||||
/// </summary>
|
||||
/// <param name="SiteCalls">The next batch of operational rows, ordered oldest-first by <see cref="SiteCall.UpdatedAtUtc"/>.</param>
|
||||
/// <param name="MoreAvailable">True when the site saturated the requested batch size — the caller should advance the cursor and pull again.</param>
|
||||
public sealed record PullSiteCallsResponse(
|
||||
IReadOnlyList<SiteCall> SiteCalls,
|
||||
bool MoreAvailable);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,6 +10,7 @@ service SiteStreamService {
|
||||
rpc IngestAuditEvents(AuditEventBatch) returns (IngestAck);
|
||||
rpc IngestCachedTelemetry(CachedTelemetryBatch) returns (IngestAck);
|
||||
rpc PullAuditEvents(PullAuditEventsRequest) returns (PullAuditEventsResponse);
|
||||
rpc PullSiteCalls(PullSiteCallsRequest) returns (PullSiteCallsResponse);
|
||||
}
|
||||
|
||||
message InstanceStreamRequest {
|
||||
@@ -157,3 +158,20 @@ message PullAuditEventsResponse {
|
||||
repeated AuditEventDto events = 1;
|
||||
bool more_available = 2;
|
||||
}
|
||||
|
||||
// Site Call Audit (#22) reconciliation pull: central→site request for any
|
||||
// site-local operation-tracking rows whose UpdatedAtUtc >= since_utc — the
|
||||
// self-heal feed that backfills the eventually-consistent central SiteCalls
|
||||
// mirror when best-effort push telemetry is lost. Mirrors PullAuditEvents
|
||||
// but is a SEPARATE RPC (the tracking store is the operational source of
|
||||
// truth, distinct from the site audit queue). more_available signals
|
||||
// batch_size was saturated so the caller advances since_utc and pulls again.
|
||||
message PullSiteCallsRequest {
|
||||
google.protobuf.Timestamp since_utc = 1;
|
||||
int32 batch_size = 2;
|
||||
}
|
||||
|
||||
message PullSiteCallsResponse {
|
||||
repeated SiteCallOperationalDto operationals = 1;
|
||||
bool more_available = 2;
|
||||
}
|
||||
|
||||
@@ -81,23 +81,30 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
"dWVzdBItCglzaW5jZV91dGMYASABKAsyGi5nb29nbGUucHJvdG9idWYuVGlt",
|
||||
"ZXN0YW1wEhIKCmJhdGNoX3NpemUYAiABKAUiXAoXUHVsbEF1ZGl0RXZlbnRz",
|
||||
"UmVzcG9uc2USKQoGZXZlbnRzGAEgAygLMhkuc2l0ZXN0cmVhbS5BdWRpdEV2",
|
||||
"ZW50RHRvEhYKDm1vcmVfYXZhaWxhYmxlGAIgASgIKlwKB1F1YWxpdHkSFwoT",
|
||||
"UVVBTElUWV9VTlNQRUNJRklFRBAAEhAKDFFVQUxJVFlfR09PRBABEhUKEVFV",
|
||||
"QUxJVFlfVU5DRVJUQUlOEAISDwoLUVVBTElUWV9CQUQQAypdCg5BbGFybVN0",
|
||||
"YXRlRW51bRIbChdBTEFSTV9TVEFURV9VTlNQRUNJRklFRBAAEhYKEkFMQVJN",
|
||||
"X1NUQVRFX05PUk1BTBABEhYKEkFMQVJNX1NUQVRFX0FDVElWRRACKoUBCg5B",
|
||||
"bGFybUxldmVsRW51bRIUChBBTEFSTV9MRVZFTF9OT05FEAASEwoPQUxBUk1f",
|
||||
"TEVWRUxfTE9XEAESFwoTQUxBUk1fTEVWRUxfTE9XX0xPVxACEhQKEEFMQVJN",
|
||||
"X0xFVkVMX0hJR0gQAxIZChVBTEFSTV9MRVZFTF9ISUdIX0hJR0gQBDLhAgoR",
|
||||
"U2l0ZVN0cmVhbVNlcnZpY2USVQoRU3Vic2NyaWJlSW5zdGFuY2USIS5zaXRl",
|
||||
"c3RyZWFtLkluc3RhbmNlU3RyZWFtUmVxdWVzdBobLnNpdGVzdHJlYW0uU2l0",
|
||||
"ZVN0cmVhbUV2ZW50MAESRwoRSW5nZXN0QXVkaXRFdmVudHMSGy5zaXRlc3Ry",
|
||||
"ZWFtLkF1ZGl0RXZlbnRCYXRjaBoVLnNpdGVzdHJlYW0uSW5nZXN0QWNrElAK",
|
||||
"FUluZ2VzdENhY2hlZFRlbGVtZXRyeRIgLnNpdGVzdHJlYW0uQ2FjaGVkVGVs",
|
||||
"ZW1ldHJ5QmF0Y2gaFS5zaXRlc3RyZWFtLkluZ2VzdEFjaxJaCg9QdWxsQXVk",
|
||||
"aXRFdmVudHMSIi5zaXRlc3RyZWFtLlB1bGxBdWRpdEV2ZW50c1JlcXVlc3Qa",
|
||||
"Iy5zaXRlc3RyZWFtLlB1bGxBdWRpdEV2ZW50c1Jlc3BvbnNlQiuqAihaQi5N",
|
||||
"T00uV1cuU2NhZGFCcmlkZ2UuQ29tbXVuaWNhdGlvbi5HcnBjYgZwcm90bzM="));
|
||||
"ZW50RHRvEhYKDm1vcmVfYXZhaWxhYmxlGAIgASgIIlkKFFB1bGxTaXRlQ2Fs",
|
||||
"bHNSZXF1ZXN0Ei0KCXNpbmNlX3V0YxgBIAEoCzIaLmdvb2dsZS5wcm90b2J1",
|
||||
"Zi5UaW1lc3RhbXASEgoKYmF0Y2hfc2l6ZRgCIAEoBSJpChVQdWxsU2l0ZUNh",
|
||||
"bGxzUmVzcG9uc2USOAoMb3BlcmF0aW9uYWxzGAEgAygLMiIuc2l0ZXN0cmVh",
|
||||
"bS5TaXRlQ2FsbE9wZXJhdGlvbmFsRHRvEhYKDm1vcmVfYXZhaWxhYmxlGAIg",
|
||||
"ASgIKlwKB1F1YWxpdHkSFwoTUVVBTElUWV9VTlNQRUNJRklFRBAAEhAKDFFV",
|
||||
"QUxJVFlfR09PRBABEhUKEVFVQUxJVFlfVU5DRVJUQUlOEAISDwoLUVVBTElU",
|
||||
"WV9CQUQQAypdCg5BbGFybVN0YXRlRW51bRIbChdBTEFSTV9TVEFURV9VTlNQ",
|
||||
"RUNJRklFRBAAEhYKEkFMQVJNX1NUQVRFX05PUk1BTBABEhYKEkFMQVJNX1NU",
|
||||
"QVRFX0FDVElWRRACKoUBCg5BbGFybUxldmVsRW51bRIUChBBTEFSTV9MRVZF",
|
||||
"TF9OT05FEAASEwoPQUxBUk1fTEVWRUxfTE9XEAESFwoTQUxBUk1fTEVWRUxf",
|
||||
"TE9XX0xPVxACEhQKEEFMQVJNX0xFVkVMX0hJR0gQAxIZChVBTEFSTV9MRVZF",
|
||||
"TF9ISUdIX0hJR0gQBDK3AwoRU2l0ZVN0cmVhbVNlcnZpY2USVQoRU3Vic2Ny",
|
||||
"aWJlSW5zdGFuY2USIS5zaXRlc3RyZWFtLkluc3RhbmNlU3RyZWFtUmVxdWVz",
|
||||
"dBobLnNpdGVzdHJlYW0uU2l0ZVN0cmVhbUV2ZW50MAESRwoRSW5nZXN0QXVk",
|
||||
"aXRFdmVudHMSGy5zaXRlc3RyZWFtLkF1ZGl0RXZlbnRCYXRjaBoVLnNpdGVz",
|
||||
"dHJlYW0uSW5nZXN0QWNrElAKFUluZ2VzdENhY2hlZFRlbGVtZXRyeRIgLnNp",
|
||||
"dGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gaFS5zaXRlc3RyZWFtLklu",
|
||||
"Z2VzdEFjaxJaCg9QdWxsQXVkaXRFdmVudHMSIi5zaXRlc3RyZWFtLlB1bGxB",
|
||||
"dWRpdEV2ZW50c1JlcXVlc3QaIy5zaXRlc3RyZWFtLlB1bGxBdWRpdEV2ZW50",
|
||||
"c1Jlc3BvbnNlElQKDVB1bGxTaXRlQ2FsbHMSIC5zaXRlc3RyZWFtLlB1bGxT",
|
||||
"aXRlQ2FsbHNSZXF1ZXN0GiEuc2l0ZXN0cmVhbS5QdWxsU2l0ZUNhbGxzUmVz",
|
||||
"cG9uc2VCK6oCKFpCLk1PTS5XVy5TY2FkYUJyaWRnZS5Db21tdW5pY2F0aW9u",
|
||||
"LkdycGNiBnByb3RvMw=="));
|
||||
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
|
||||
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.WrappersReflection.Descriptor, },
|
||||
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.Quality), typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmStateEnum), typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AlarmLevelEnum), }, null, new pbr::GeneratedClrTypeInfo[] {
|
||||
@@ -112,7 +119,9 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.CachedTelemetryPacket), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.CachedTelemetryPacket.Parser, new[]{ "AuditEvent", "Operational" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.CachedTelemetryBatch), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.CachedTelemetryBatch.Parser, new[]{ "Packets" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsRequest), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsRequest.Parser, new[]{ "SinceUtc", "BatchSize" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsResponse), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsResponse.Parser, new[]{ "Events", "MoreAvailable" }, null, null, null, null)
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsResponse), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsResponse.Parser, new[]{ "Events", "MoreAvailable" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest.Parser, new[]{ "SinceUtc", "BatchSize" }, null, null, null, null),
|
||||
new pbr::GeneratedClrTypeInfo(typeof(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse), global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse.Parser, new[]{ "Operationals", "MoreAvailable" }, null, null, null, null)
|
||||
}));
|
||||
}
|
||||
#endregion
|
||||
@@ -5064,6 +5073,483 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Site Call Audit (#22) reconciliation pull: central→site request for any
|
||||
/// site-local operation-tracking rows whose UpdatedAtUtc >= since_utc — the
|
||||
/// self-heal feed that backfills the eventually-consistent central SiteCalls
|
||||
/// mirror when best-effort push telemetry is lost. Mirrors PullAuditEvents
|
||||
/// but is a SEPARATE RPC (the tracking store is the operational source of
|
||||
/// truth, distinct from the site audit queue). more_available signals
|
||||
/// batch_size was saturated so the caller advances since_utc and pulls again.
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
|
||||
public sealed partial class PullSiteCallsRequest : pb::IMessage<PullSiteCallsRequest>
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
, pb::IBufferMessage
|
||||
#endif
|
||||
{
|
||||
private static readonly pb::MessageParser<PullSiteCallsRequest> _parser = new pb::MessageParser<PullSiteCallsRequest>(() => new PullSiteCallsRequest());
|
||||
private pb::UnknownFieldSet _unknownFields;
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public static pb::MessageParser<PullSiteCallsRequest> Parser { get { return _parser; } }
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public static pbr::MessageDescriptor Descriptor {
|
||||
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[12]; }
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
pbr::MessageDescriptor pb::IMessage.Descriptor {
|
||||
get { return Descriptor; }
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public PullSiteCallsRequest() {
|
||||
OnConstruction();
|
||||
}
|
||||
|
||||
partial void OnConstruction();
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public PullSiteCallsRequest(PullSiteCallsRequest other) : this() {
|
||||
sinceUtc_ = other.sinceUtc_ != null ? other.sinceUtc_.Clone() : null;
|
||||
batchSize_ = other.batchSize_;
|
||||
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public PullSiteCallsRequest Clone() {
|
||||
return new PullSiteCallsRequest(this);
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "since_utc" field.</summary>
|
||||
public const int SinceUtcFieldNumber = 1;
|
||||
private global::Google.Protobuf.WellKnownTypes.Timestamp sinceUtc_;
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public global::Google.Protobuf.WellKnownTypes.Timestamp SinceUtc {
|
||||
get { return sinceUtc_; }
|
||||
set {
|
||||
sinceUtc_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "batch_size" field.</summary>
|
||||
public const int BatchSizeFieldNumber = 2;
|
||||
private int batchSize_;
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public int BatchSize {
|
||||
get { return batchSize_; }
|
||||
set {
|
||||
batchSize_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public override bool Equals(object other) {
|
||||
return Equals(other as PullSiteCallsRequest);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public bool Equals(PullSiteCallsRequest other) {
|
||||
if (ReferenceEquals(other, null)) {
|
||||
return false;
|
||||
}
|
||||
if (ReferenceEquals(other, this)) {
|
||||
return true;
|
||||
}
|
||||
if (!object.Equals(SinceUtc, other.SinceUtc)) return false;
|
||||
if (BatchSize != other.BatchSize) return false;
|
||||
return Equals(_unknownFields, other._unknownFields);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public override int GetHashCode() {
|
||||
int hash = 1;
|
||||
if (sinceUtc_ != null) hash ^= SinceUtc.GetHashCode();
|
||||
if (BatchSize != 0) hash ^= BatchSize.GetHashCode();
|
||||
if (_unknownFields != null) {
|
||||
hash ^= _unknownFields.GetHashCode();
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public override string ToString() {
|
||||
return pb::JsonFormatter.ToDiagnosticString(this);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public void WriteTo(pb::CodedOutputStream output) {
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
output.WriteRawMessage(this);
|
||||
#else
|
||||
if (sinceUtc_ != null) {
|
||||
output.WriteRawTag(10);
|
||||
output.WriteMessage(SinceUtc);
|
||||
}
|
||||
if (BatchSize != 0) {
|
||||
output.WriteRawTag(16);
|
||||
output.WriteInt32(BatchSize);
|
||||
}
|
||||
if (_unknownFields != null) {
|
||||
_unknownFields.WriteTo(output);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
|
||||
if (sinceUtc_ != null) {
|
||||
output.WriteRawTag(10);
|
||||
output.WriteMessage(SinceUtc);
|
||||
}
|
||||
if (BatchSize != 0) {
|
||||
output.WriteRawTag(16);
|
||||
output.WriteInt32(BatchSize);
|
||||
}
|
||||
if (_unknownFields != null) {
|
||||
_unknownFields.WriteTo(ref output);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public int CalculateSize() {
|
||||
int size = 0;
|
||||
if (sinceUtc_ != null) {
|
||||
size += 1 + pb::CodedOutputStream.ComputeMessageSize(SinceUtc);
|
||||
}
|
||||
if (BatchSize != 0) {
|
||||
size += 1 + pb::CodedOutputStream.ComputeInt32Size(BatchSize);
|
||||
}
|
||||
if (_unknownFields != null) {
|
||||
size += _unknownFields.CalculateSize();
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public void MergeFrom(PullSiteCallsRequest other) {
|
||||
if (other == null) {
|
||||
return;
|
||||
}
|
||||
if (other.sinceUtc_ != null) {
|
||||
if (sinceUtc_ == null) {
|
||||
SinceUtc = new global::Google.Protobuf.WellKnownTypes.Timestamp();
|
||||
}
|
||||
SinceUtc.MergeFrom(other.SinceUtc);
|
||||
}
|
||||
if (other.BatchSize != 0) {
|
||||
BatchSize = other.BatchSize;
|
||||
}
|
||||
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public void MergeFrom(pb::CodedInputStream input) {
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
input.ReadRawMessage(this);
|
||||
#else
|
||||
uint tag;
|
||||
while ((tag = input.ReadTag()) != 0) {
|
||||
if ((tag & 7) == 4) {
|
||||
// Abort on any end group tag.
|
||||
return;
|
||||
}
|
||||
switch(tag) {
|
||||
default:
|
||||
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
|
||||
break;
|
||||
case 10: {
|
||||
if (sinceUtc_ == null) {
|
||||
SinceUtc = new global::Google.Protobuf.WellKnownTypes.Timestamp();
|
||||
}
|
||||
input.ReadMessage(SinceUtc);
|
||||
break;
|
||||
}
|
||||
case 16: {
|
||||
BatchSize = input.ReadInt32();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
|
||||
uint tag;
|
||||
while ((tag = input.ReadTag()) != 0) {
|
||||
if ((tag & 7) == 4) {
|
||||
// Abort on any end group tag.
|
||||
return;
|
||||
}
|
||||
switch(tag) {
|
||||
default:
|
||||
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
|
||||
break;
|
||||
case 10: {
|
||||
if (sinceUtc_ == null) {
|
||||
SinceUtc = new global::Google.Protobuf.WellKnownTypes.Timestamp();
|
||||
}
|
||||
input.ReadMessage(SinceUtc);
|
||||
break;
|
||||
}
|
||||
case 16: {
|
||||
BatchSize = input.ReadInt32();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
|
||||
public sealed partial class PullSiteCallsResponse : pb::IMessage<PullSiteCallsResponse>
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
, pb::IBufferMessage
|
||||
#endif
|
||||
{
|
||||
private static readonly pb::MessageParser<PullSiteCallsResponse> _parser = new pb::MessageParser<PullSiteCallsResponse>(() => new PullSiteCallsResponse());
|
||||
private pb::UnknownFieldSet _unknownFields;
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public static pb::MessageParser<PullSiteCallsResponse> Parser { get { return _parser; } }
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public static pbr::MessageDescriptor Descriptor {
|
||||
get { return global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SitestreamReflection.Descriptor.MessageTypes[13]; }
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
pbr::MessageDescriptor pb::IMessage.Descriptor {
|
||||
get { return Descriptor; }
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public PullSiteCallsResponse() {
|
||||
OnConstruction();
|
||||
}
|
||||
|
||||
partial void OnConstruction();
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public PullSiteCallsResponse(PullSiteCallsResponse other) : this() {
|
||||
operationals_ = other.operationals_.Clone();
|
||||
moreAvailable_ = other.moreAvailable_;
|
||||
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public PullSiteCallsResponse Clone() {
|
||||
return new PullSiteCallsResponse(this);
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "operationals" field.</summary>
|
||||
public const int OperationalsFieldNumber = 1;
|
||||
private static readonly pb::FieldCodec<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteCallOperationalDto> _repeated_operationals_codec
|
||||
= pb::FieldCodec.ForMessage(10, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteCallOperationalDto.Parser);
|
||||
private readonly pbc::RepeatedField<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteCallOperationalDto> operationals_ = new pbc::RepeatedField<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteCallOperationalDto>();
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public pbc::RepeatedField<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteCallOperationalDto> Operationals {
|
||||
get { return operationals_; }
|
||||
}
|
||||
|
||||
/// <summary>Field number for the "more_available" field.</summary>
|
||||
public const int MoreAvailableFieldNumber = 2;
|
||||
private bool moreAvailable_;
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public bool MoreAvailable {
|
||||
get { return moreAvailable_; }
|
||||
set {
|
||||
moreAvailable_ = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public override bool Equals(object other) {
|
||||
return Equals(other as PullSiteCallsResponse);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public bool Equals(PullSiteCallsResponse other) {
|
||||
if (ReferenceEquals(other, null)) {
|
||||
return false;
|
||||
}
|
||||
if (ReferenceEquals(other, this)) {
|
||||
return true;
|
||||
}
|
||||
if(!operationals_.Equals(other.operationals_)) return false;
|
||||
if (MoreAvailable != other.MoreAvailable) return false;
|
||||
return Equals(_unknownFields, other._unknownFields);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public override int GetHashCode() {
|
||||
int hash = 1;
|
||||
hash ^= operationals_.GetHashCode();
|
||||
if (MoreAvailable != false) hash ^= MoreAvailable.GetHashCode();
|
||||
if (_unknownFields != null) {
|
||||
hash ^= _unknownFields.GetHashCode();
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public override string ToString() {
|
||||
return pb::JsonFormatter.ToDiagnosticString(this);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public void WriteTo(pb::CodedOutputStream output) {
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
output.WriteRawMessage(this);
|
||||
#else
|
||||
operationals_.WriteTo(output, _repeated_operationals_codec);
|
||||
if (MoreAvailable != false) {
|
||||
output.WriteRawTag(16);
|
||||
output.WriteBool(MoreAvailable);
|
||||
}
|
||||
if (_unknownFields != null) {
|
||||
_unknownFields.WriteTo(output);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
|
||||
operationals_.WriteTo(ref output, _repeated_operationals_codec);
|
||||
if (MoreAvailable != false) {
|
||||
output.WriteRawTag(16);
|
||||
output.WriteBool(MoreAvailable);
|
||||
}
|
||||
if (_unknownFields != null) {
|
||||
_unknownFields.WriteTo(ref output);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public int CalculateSize() {
|
||||
int size = 0;
|
||||
size += operationals_.CalculateSize(_repeated_operationals_codec);
|
||||
if (MoreAvailable != false) {
|
||||
size += 1 + 1;
|
||||
}
|
||||
if (_unknownFields != null) {
|
||||
size += _unknownFields.CalculateSize();
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public void MergeFrom(PullSiteCallsResponse other) {
|
||||
if (other == null) {
|
||||
return;
|
||||
}
|
||||
operationals_.Add(other.operationals_);
|
||||
if (other.MoreAvailable != false) {
|
||||
MoreAvailable = other.MoreAvailable;
|
||||
}
|
||||
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
||||
}
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public void MergeFrom(pb::CodedInputStream input) {
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
input.ReadRawMessage(this);
|
||||
#else
|
||||
uint tag;
|
||||
while ((tag = input.ReadTag()) != 0) {
|
||||
if ((tag & 7) == 4) {
|
||||
// Abort on any end group tag.
|
||||
return;
|
||||
}
|
||||
switch(tag) {
|
||||
default:
|
||||
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
|
||||
break;
|
||||
case 10: {
|
||||
operationals_.AddEntriesFrom(input, _repeated_operationals_codec);
|
||||
break;
|
||||
}
|
||||
case 16: {
|
||||
MoreAvailable = input.ReadBool();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
|
||||
uint tag;
|
||||
while ((tag = input.ReadTag()) != 0) {
|
||||
if ((tag & 7) == 4) {
|
||||
// Abort on any end group tag.
|
||||
return;
|
||||
}
|
||||
switch(tag) {
|
||||
default:
|
||||
_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
|
||||
break;
|
||||
case 10: {
|
||||
operationals_.AddEntriesFrom(ref input, _repeated_operationals_codec);
|
||||
break;
|
||||
}
|
||||
case 16: {
|
||||
MoreAvailable = input.ReadBool();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
static readonly grpc::Marshaller<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsRequest> __Marshaller_sitestream_PullAuditEventsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsRequest.Parser));
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static readonly grpc::Marshaller<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsResponse> __Marshaller_sitestream_PullAuditEventsResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsResponse.Parser));
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static readonly grpc::Marshaller<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest> __Marshaller_sitestream_PullSiteCallsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest.Parser));
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static readonly grpc::Marshaller<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse> __Marshaller_sitestream_PullSiteCallsResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse.Parser));
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static readonly grpc::Method<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.InstanceStreamRequest, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent> __Method_SubscribeInstance = new grpc::Method<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.InstanceStreamRequest, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamEvent>(
|
||||
@@ -92,6 +96,14 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
__Marshaller_sitestream_PullAuditEventsRequest,
|
||||
__Marshaller_sitestream_PullAuditEventsResponse);
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
static readonly grpc::Method<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse> __Method_PullSiteCalls = new grpc::Method<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse>(
|
||||
grpc::MethodType.Unary,
|
||||
__ServiceName,
|
||||
"PullSiteCalls",
|
||||
__Marshaller_sitestream_PullSiteCallsRequest,
|
||||
__Marshaller_sitestream_PullSiteCallsResponse);
|
||||
|
||||
/// <summary>Service descriptor</summary>
|
||||
public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor
|
||||
{
|
||||
@@ -126,6 +138,12 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
|
||||
}
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public virtual global::System.Threading.Tasks.Task<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse> PullSiteCalls(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest request, grpc::ServerCallContext context)
|
||||
{
|
||||
throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Client for SiteStreamService</summary>
|
||||
@@ -225,6 +243,26 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
{
|
||||
return CallInvoker.AsyncUnaryCall(__Method_PullAuditEvents, null, options, request);
|
||||
}
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public virtual global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse PullSiteCalls(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
|
||||
{
|
||||
return PullSiteCalls(request, new grpc::CallOptions(headers, deadline, cancellationToken));
|
||||
}
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public virtual global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse PullSiteCalls(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest request, grpc::CallOptions options)
|
||||
{
|
||||
return CallInvoker.BlockingUnaryCall(__Method_PullSiteCalls, null, options, request);
|
||||
}
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse> PullSiteCallsAsync(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))
|
||||
{
|
||||
return PullSiteCallsAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));
|
||||
}
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
public virtual grpc::AsyncUnaryCall<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse> PullSiteCallsAsync(global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest request, grpc::CallOptions options)
|
||||
{
|
||||
return CallInvoker.AsyncUnaryCall(__Method_PullSiteCalls, null, options, request);
|
||||
}
|
||||
/// <summary>Creates a new instance of client from given <c>ClientBaseConfiguration</c>.</summary>
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
|
||||
protected override SiteStreamServiceClient NewInstance(ClientBaseConfiguration configuration)
|
||||
@@ -242,7 +280,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
.AddMethod(__Method_SubscribeInstance, serviceImpl.SubscribeInstance)
|
||||
.AddMethod(__Method_IngestAuditEvents, serviceImpl.IngestAuditEvents)
|
||||
.AddMethod(__Method_IngestCachedTelemetry, serviceImpl.IngestCachedTelemetry)
|
||||
.AddMethod(__Method_PullAuditEvents, serviceImpl.PullAuditEvents).Build();
|
||||
.AddMethod(__Method_PullAuditEvents, serviceImpl.PullAuditEvents)
|
||||
.AddMethod(__Method_PullSiteCalls, serviceImpl.PullSiteCalls).Build();
|
||||
}
|
||||
|
||||
/// <summary>Register service method with a service binder with or without implementation. Useful when customizing the service binding logic.
|
||||
@@ -256,6 +295,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
|
||||
serviceBinder.AddMethod(__Method_IngestAuditEvents, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.AuditEventBatch, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.IngestAck>(serviceImpl.IngestAuditEvents));
|
||||
serviceBinder.AddMethod(__Method_IngestCachedTelemetry, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.CachedTelemetryBatch, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.IngestAck>(serviceImpl.IngestCachedTelemetry));
|
||||
serviceBinder.AddMethod(__Method_PullAuditEvents, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsRequest, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEventsResponse>(serviceImpl.PullAuditEvents));
|
||||
serviceBinder.AddMethod(__Method_PullSiteCalls, serviceImpl == null ? null : new grpc::UnaryServerMethod<global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsRequest, global::ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullSiteCallsResponse>(serviceImpl.PullSiteCalls));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1009,6 +1009,18 @@ akka {{
|
||||
// direction one-way (Host knows both; Communication doesn't reach back
|
||||
// into AuditLog).
|
||||
grpcServer?.SetSiteAuditQueue(siteAuditQueue);
|
||||
// Site Call Audit (#22): hand the site-local OperationTrackingStore to
|
||||
// the gRPC server so the PullSiteCalls reconciliation RPC can serve
|
||||
// central's self-heal pulls. siteTrackingStore is resolved above with
|
||||
// GetService — present on site composition roots, null on central — so
|
||||
// wire the seam only when the store exists. Like SetSiteAuditQueue, both
|
||||
// the store and the gRPC server are singletons; wiring here keeps the
|
||||
// dependency direction one-way (Host knows both; Communication doesn't
|
||||
// reach back into SiteRuntime).
|
||||
if (siteTrackingStore is not null)
|
||||
{
|
||||
grpcServer?.SetOperationTrackingStore(siteTrackingStore);
|
||||
}
|
||||
grpcServer?.SetReady(_actorSystem!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +360,76 @@ public class OperationTrackingStore : IOperationTrackingStore, IAsyncDisposable,
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IReadOnlyList<SiteCallOperational>> ReadChangedSinceAsync(
|
||||
DateTime sinceUtc,
|
||||
int batchSize,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposeState) != 0, this);
|
||||
|
||||
// SiteRuntime-024: like GetStatusAsync, the reconciliation pull opens a
|
||||
// fresh, ungated read connection so a long-running write never blocks
|
||||
// central's PullSiteCalls. The query is a bounded, ordered scan over the
|
||||
// (Status, UpdatedAtUtc) index range — UpdatedAtUtc is the cursor.
|
||||
await using var readConnection = new SqliteConnection(_connectionString);
|
||||
await readConnection.OpenAsync(ct).ConfigureAwait(false);
|
||||
|
||||
await using var cmd = readConnection.CreateCommand();
|
||||
// Inclusive lower bound on UpdatedAtUtc (>=) so a caller resuming from
|
||||
// the last returned timestamp does not skip a row sharing that instant;
|
||||
// central ingest is insert-if-not-exists + upsert-on-newer, so the
|
||||
// boundary row re-read is a no-op. ORDER BY ... ASC + LIMIT yields the
|
||||
// OLDEST matching rows so the cursor advances monotonically.
|
||||
cmd.CommandText = """
|
||||
SELECT TrackedOperationId, Kind, TargetSummary, Status,
|
||||
RetryCount, LastError, HttpStatus,
|
||||
CreatedAtUtc, UpdatedAtUtc, TerminalAtUtc, SourceNode
|
||||
FROM OperationTracking
|
||||
WHERE UpdatedAtUtc >= $since
|
||||
ORDER BY UpdatedAtUtc ASC
|
||||
LIMIT $batchSize;
|
||||
""";
|
||||
cmd.Parameters.AddWithValue(
|
||||
"$since",
|
||||
sinceUtc.ToString("o", CultureInfo.InvariantCulture));
|
||||
cmd.Parameters.AddWithValue("$batchSize", batchSize);
|
||||
|
||||
var rows = new List<SiteCallOperational>();
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
var kind = reader.GetString(1);
|
||||
rows.Add(new SiteCallOperational(
|
||||
TrackedOperationId: TrackedOperationId.Parse(reader.GetString(0)),
|
||||
Channel: KindToChannel(kind),
|
||||
Target: reader.IsDBNull(2) ? string.Empty : reader.GetString(2),
|
||||
// The site id is not a tracking-store column; the central client
|
||||
// re-stamps SourceSite from the siteId it dialed.
|
||||
SourceSite: string.Empty,
|
||||
SourceNode: reader.IsDBNull(10) ? null : reader.GetString(10),
|
||||
Status: reader.GetString(3),
|
||||
RetryCount: reader.GetInt32(4),
|
||||
LastError: reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||
HttpStatus: reader.IsDBNull(6) ? null : reader.GetInt32(6),
|
||||
CreatedAtUtc: ParseUtc(reader.GetString(7)),
|
||||
UpdatedAtUtc: ParseUtc(reader.GetString(8)),
|
||||
TerminalAtUtc: reader.IsDBNull(9) ? null : ParseUtc(reader.GetString(9))));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Cached-call Kind → SiteCalls Channel. Only ApiCallCached / DbWriteCached
|
||||
// ever reach the tracking store (RecordEnqueueAsync is the cached-call
|
||||
// entry point); DbWriteCached maps to DbOutbound, everything else to the
|
||||
// ApiOutbound default. Mirrors CachedCallLifecycleBridge's channel handling.
|
||||
private static string KindToChannel(string kind) => kind switch
|
||||
{
|
||||
nameof(Commons.Types.Enums.AuditKind.DbWriteCached) => nameof(Commons.Types.Enums.AuditChannel.DbOutbound),
|
||||
_ => nameof(Commons.Types.Enums.AuditChannel.ApiOutbound),
|
||||
};
|
||||
|
||||
private static DateTime ParseUtc(string raw)
|
||||
{
|
||||
return DateTime.Parse(
|
||||
|
||||
Reference in New Issue
Block a user