|
|
|
@@ -13,7 +13,7 @@ using ZB.MOM.WW.ScadaBridge.Communication;
|
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteCallAudit;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Central singleton for Site Call Audit (#22). Receives
|
|
|
|
|
/// Central singleton for Site Call Audit. Receives
|
|
|
|
|
/// <see cref="UpsertSiteCallCommand"/> messages and persists each
|
|
|
|
|
/// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit.SiteCall"/> row via
|
|
|
|
|
/// <see cref="ISiteCallAuditRepository.UpsertAsync"/> — idempotent monotonic
|
|
|
|
@@ -25,15 +25,15 @@ namespace ZB.MOM.WW.ScadaBridge.SiteCallAudit;
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// <para>
|
|
|
|
|
/// Implemented: direct <see cref="UpsertSiteCallCommand"/> telemetry ingest,
|
|
|
|
|
/// query, detail and KPI handlers (Task 4), the central→site Retry/Discard
|
|
|
|
|
/// relay (Task 5 — the relay handlers live in this actor), the periodic
|
|
|
|
|
/// query, detail and KPI handlers, the central→site Retry/Discard
|
|
|
|
|
/// relay (the relay handlers live in this actor), the periodic
|
|
|
|
|
/// per-site reconciliation puller that backfills lost telemetry (Piece A —
|
|
|
|
|
/// <see cref="OnReconciliationTickAsync"/>, the documented self-heal pull), and
|
|
|
|
|
/// the daily terminal-row purge scheduler (Piece B —
|
|
|
|
|
/// <see cref="OnPurgeTickAsync"/>, which invokes
|
|
|
|
|
/// <see cref="ISiteCallAuditRepository.PurgeTerminalAsync"/> on a timer). Both
|
|
|
|
|
/// background timers are started in <see cref="PreStart"/>, but on independent
|
|
|
|
|
/// preconditions (SiteCallAudit-007). The purge timer is armed whenever
|
|
|
|
|
/// preconditions. The purge timer is armed whenever
|
|
|
|
|
/// background timers are enabled (it needs only the repository, which every
|
|
|
|
|
/// production / reconciliation ctor always has) — it is NOT gated on the
|
|
|
|
|
/// reconciliation collaborators, so a host that registers Site Call Audit
|
|
|
|
@@ -63,7 +63,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteCallAudit;
|
|
|
|
|
/// </para>
|
|
|
|
|
/// <para>
|
|
|
|
|
/// Two constructors exist for the same reason as
|
|
|
|
|
/// <c>AuditLogIngestActor</c>: production wiring (Bundle F) resolves the
|
|
|
|
|
/// <c>AuditLogIngestActor</c>: production wiring resolves the
|
|
|
|
|
/// scoped EF repository from a fresh DI scope per message because the actor
|
|
|
|
|
/// is a long-lived cluster singleton, while tests inject a concrete
|
|
|
|
|
/// <see cref="ISiteCallAuditRepository"/> against a per-test MSSQL fixture
|
|
|
|
@@ -76,7 +76,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
private const int MaxPageSize = 200;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// SiteCallAudit-009: hard ceiling on the number of <c>PullSiteCalls</c> RPCs
|
|
|
|
|
/// Hard ceiling on the number of <c>PullSiteCalls</c> RPCs
|
|
|
|
|
/// issued for a single site within ONE reconciliation tick when the site keeps
|
|
|
|
|
/// reporting <see cref="PullSiteCallsResponse.MoreAvailable"/>. Bounds the
|
|
|
|
|
/// within-tick continuation drain so a misbehaving site (or a pathological
|
|
|
|
@@ -99,8 +99,8 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
/// in the test path they are injected directly. They are <c>null</c> when
|
|
|
|
|
/// the actor was built via the repo-only test ctor — in that case the
|
|
|
|
|
/// reconciliation tick is NOT started (see <see cref="StartReconciliationTimer"/>).
|
|
|
|
|
/// The purge tick, by contrast, does NOT depend on these collaborators
|
|
|
|
|
/// (SiteCallAudit-007): it needs only the repository, so it is armed by
|
|
|
|
|
/// The purge tick, by contrast, does NOT depend on these collaborators:
|
|
|
|
|
/// it needs only the repository, so it is armed by
|
|
|
|
|
/// <see cref="_backgroundTimersEnabled"/> alone (see <see cref="StartPurgeTimer"/>).
|
|
|
|
|
/// </summary>
|
|
|
|
|
private readonly IPullSiteCallsClient? _pullClient;
|
|
|
|
@@ -109,7 +109,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Master switch for the two background schedulers (reconciliation + purge),
|
|
|
|
|
/// set <c>true</c> by the production and reconciliation ctors and <c>false</c>
|
|
|
|
|
/// by the repo-only MSSQL test ctor. SiteCallAudit-007: the purge timer is
|
|
|
|
|
/// by the repo-only MSSQL test ctor. The purge timer is
|
|
|
|
|
/// gated on THIS flag rather than on the reconciliation collaborators, so a
|
|
|
|
|
/// host that omits the reconciliation client still purges (no unbounded
|
|
|
|
|
/// central <c>SiteCalls</c> growth) while the MSSQL read/upsert tests stay
|
|
|
|
@@ -134,7 +134,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
private ICancelable? _purgeTimer;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Task 5 (#22): the central→site command transport — the
|
|
|
|
|
/// The central→site command transport — the
|
|
|
|
|
/// <c>CentralCommunicationActor</c>, which owns the per-site
|
|
|
|
|
/// <c>ClusterClient</c> map and routes a <see cref="SiteEnvelope"/> to the
|
|
|
|
|
/// owning site. Set via <see cref="RegisterCentralCommunication"/> by the
|
|
|
|
@@ -149,7 +149,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Test-mode constructor — injects a concrete repository instance whose
|
|
|
|
|
/// lifetime exceeds the test, so the actor reuses the same instance
|
|
|
|
|
/// across every message. Used by Bundle C's MSSQL-backed TestKit fixture.
|
|
|
|
|
/// across every message. Used by the MSSQL-backed TestKit fixture.
|
|
|
|
|
/// An optional <paramref name="options"/> lets a test pin the stuck/KPI
|
|
|
|
|
/// windows; when omitted the production defaults apply.
|
|
|
|
|
/// <para>
|
|
|
|
@@ -176,7 +176,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
_options = options ?? new SiteCallAuditOptions();
|
|
|
|
|
|
|
|
|
|
// Repo-only MSSQL test ctor: keep BOTH background timers off so the
|
|
|
|
|
// read/upsert tests see no scheduled side effects (SiteCallAudit-007).
|
|
|
|
|
// read/upsert tests see no scheduled side effects.
|
|
|
|
|
_backgroundTimersEnabled = false;
|
|
|
|
|
|
|
|
|
|
RegisterHandlers();
|
|
|
|
@@ -267,15 +267,15 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
// Production path: background timers run. The purge tick is armed
|
|
|
|
|
// unconditionally here (it needs only the repository); the reconciliation
|
|
|
|
|
// tick additionally requires its collaborators and logs a Warning if
|
|
|
|
|
// they were not registered (SiteCallAudit-007).
|
|
|
|
|
// they were not registered.
|
|
|
|
|
_backgroundTimersEnabled = true;
|
|
|
|
|
|
|
|
|
|
RegisterHandlers();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Wires up the message handlers shared by both constructors: the M3
|
|
|
|
|
/// ingest path plus the Task 4 read-side (query, detail, global + per-site
|
|
|
|
|
/// Wires up the message handlers shared by both constructors: the
|
|
|
|
|
/// ingest path plus the read-side (query, detail, global + per-site
|
|
|
|
|
/// KPI). All read handlers reply to an Ask, so they capture <c>Sender</c>
|
|
|
|
|
/// before the first await and <c>PipeTo</c> the result back.
|
|
|
|
|
/// </summary>
|
|
|
|
@@ -288,7 +288,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
Receive<PerSiteSiteCallKpiRequest>(HandlePerSiteKpi);
|
|
|
|
|
Receive<PerNodeSiteCallKpiRequest>(HandlePerNodeKpi);
|
|
|
|
|
|
|
|
|
|
// Task 5 (#22): central→site Retry/Discard relay for parked cached calls.
|
|
|
|
|
// Central→site Retry/Discard relay for parked cached calls.
|
|
|
|
|
Receive<RegisterCentralCommunication>(msg =>
|
|
|
|
|
{
|
|
|
|
|
_centralCommunication = msg.CentralCommunication;
|
|
|
|
@@ -297,7 +297,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
Receive<RetrySiteCallRequest>(HandleRetrySiteCall);
|
|
|
|
|
Receive<DiscardSiteCallRequest>(HandleDiscardSiteCall);
|
|
|
|
|
|
|
|
|
|
// Piece A/B (#22): self-ticks for the periodic reconciliation pull and
|
|
|
|
|
// Piece A/B: self-ticks for the periodic reconciliation pull and
|
|
|
|
|
// the daily terminal-row purge. Handlers stay alive across faults via
|
|
|
|
|
// their own per-site / per-tick try/catch (mirroring the ingest path);
|
|
|
|
|
// the timers are only started when their collaborators are available.
|
|
|
|
@@ -327,7 +327,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
/// available. The repo-only test ctor disables background timers, so the tick
|
|
|
|
|
/// is gated off there (the MSSQL read/upsert tests must not fire phantom
|
|
|
|
|
/// pulls); the reconciliation test ctor and the production ctor (which
|
|
|
|
|
/// resolves both from the SP) start it. SiteCallAudit-007: when background
|
|
|
|
|
/// resolves both from the SP) start it. When background
|
|
|
|
|
/// timers are enabled but the collaborators were not registered, log a
|
|
|
|
|
/// Warning so a misconfigured host surfaces the missing self-heal rather than
|
|
|
|
|
/// silently skipping it.
|
|
|
|
@@ -359,7 +359,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Starts the daily purge tick. SiteCallAudit-007: the purge needs ONLY the
|
|
|
|
|
/// Starts the daily purge tick. The purge needs ONLY the
|
|
|
|
|
/// repository — never the reconciliation collaborators — so it is gated on
|
|
|
|
|
/// <see cref="_backgroundTimersEnabled"/> alone, NOT on
|
|
|
|
|
/// <see cref="_pullClient"/> / <see cref="_siteEnumerator"/>. This decouples
|
|
|
|
@@ -418,7 +418,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// SiteCallAudit-003: stamp IngestedAtUtc at central-side persist
|
|
|
|
|
// Stamp IngestedAtUtc at central-side persist
|
|
|
|
|
// time on every upsert, mirroring AuditLogIngestActor's combined-
|
|
|
|
|
// telemetry hot path. IngestedAtUtc is the "central ingested (or
|
|
|
|
|
// last refreshed) this row" timestamp; callers (telemetry,
|
|
|
|
@@ -487,7 +487,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AuditLog-003: open the scope INLINE with CreateAsyncScope + await using
|
|
|
|
|
// Open the scope INLINE with CreateAsyncScope + await using
|
|
|
|
|
// so the scoped EF Core repository (an IAsyncDisposable DbContext) disposes
|
|
|
|
|
// asynchronously at end of tick rather than blocking the Akka dispatcher
|
|
|
|
|
// thread on a synchronous Dispose() of pending connection cleanup — the tick
|
|
|
|
@@ -546,7 +546,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// <para>
|
|
|
|
|
/// <b>Coarse per-site retry — a deliberate divergence from
|
|
|
|
|
/// <c>SiteAuditReconciliationActor</c>.</b> That sibling (AuditLog-004) tracks
|
|
|
|
|
/// <c>SiteAuditReconciliationActor</c>.</b> That sibling tracks
|
|
|
|
|
/// a per-EventId attempt counter and permanently abandons a row after a
|
|
|
|
|
/// threshold so a single un-insertable row cannot block a site's cursor
|
|
|
|
|
/// forever. This actor deliberately does NOT: any throw inside the loop
|
|
|
|
@@ -568,7 +568,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
/// contract as <c>SiteAuditReconciliationActor</c>'s cursor.
|
|
|
|
|
/// </para>
|
|
|
|
|
/// <para>
|
|
|
|
|
/// <b>SiteCallAudit-009: consumes <see cref="PullSiteCallsResponse.MoreAvailable"/>
|
|
|
|
|
/// <b>Consumes <see cref="PullSiteCallsResponse.MoreAvailable"/>
|
|
|
|
|
/// to guarantee forward progress.</b> Whereas the prior implementation ignored
|
|
|
|
|
/// the flag entirely and relied solely on the tick cadence, this method now
|
|
|
|
|
/// continues pulling within the same tick while the site reports
|
|
|
|
@@ -593,7 +593,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
{
|
|
|
|
|
var cursor = _reconciliationCursors.TryGetValue(site.SiteId, out var c) ? c : DateTime.MinValue;
|
|
|
|
|
|
|
|
|
|
// SiteCallAudit-009: drain within the tick while the site keeps reporting
|
|
|
|
|
// Drain within the tick while the site keeps reporting
|
|
|
|
|
// MoreAvailable, bounded by MaxReconciliationPagesPerTick so a misbehaving
|
|
|
|
|
// site can never spin the dispatcher. Each page advances the in-flight
|
|
|
|
|
// cursor; a saturated page that fails to advance the cursor is the
|
|
|
|
@@ -683,7 +683,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
{
|
|
|
|
|
var threshold = DateTime.UtcNow - TimeSpan.FromDays(_options.RetentionDays);
|
|
|
|
|
|
|
|
|
|
// AuditLog-003: open the scope INLINE with CreateAsyncScope + await using
|
|
|
|
|
// Open the scope INLINE with CreateAsyncScope + await using
|
|
|
|
|
// so the scoped EF Core repository (an IAsyncDisposable DbContext) disposes
|
|
|
|
|
// asynchronously rather than blocking the Akka dispatcher thread on a
|
|
|
|
|
// synchronous Dispose(). Mirrors SiteAuditReconciliationActor; the
|
|
|
|
@@ -730,7 +730,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Task 4: read-side (query / detail / KPI) ──
|
|
|
|
|
// ── Read-side (query / detail / KPI) ──
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Handles a paginated, filtered query over the <c>SiteCalls</c> table.
|
|
|
|
@@ -991,7 +991,7 @@ public class SiteCallAuditActor : ReceiveActor
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Task 5: central→site Retry/Discard relay ──
|
|
|
|
|
// ── Central→site Retry/Discard relay ──
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Relays an operator Retry of a parked cached call to its owning site. The
|
|
|
|
|