docs(comments): strip internal task/milestone/bundle bookkeeping from code comments

Remove project bookkeeping citations from shipped code comments across the
solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/
issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels,
and C/D/K/S/T phase labels.

Comment text only — no code logic, string/log literals, or XML-doc structure
changed. Genuine descriptions are preserved (only the citation is stripped),
and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365,
UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker
TaskReferenceInComment / TrackingReferenceInComment checks plus targeted
grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
Joseph Doherty
2026-07-07 11:03:26 -04:00
parent 67005ca4c0
commit 9cff87fe85
435 changed files with 2338 additions and 2547 deletions
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E (T8, T9) — central singleton implementation of
/// Central singleton implementation of
/// <see cref="IAuditCentralHealthSnapshot"/>. Owns thread-safe
/// <see cref="System.Threading.Interlocked"/> counters for
/// <c>CentralAuditWriteFailures</c> + <c>AuditRedactionFailure</c> and a
@@ -71,7 +71,7 @@ public class AuditLogIngestActor : ReceiveActor
_logger = logger;
ReceiveAsync<IngestAuditEventsCommand>(OnIngestAsync);
// The single-repository test ctor cannot service the M3 dual-write —
// The single-repository test ctor cannot service the dual-write —
// it has no SiteCalls repo and no DbContext. The handler still
// registers (so callers don't dead-letter) but replies empty so the
// test surface stays explicit about what this ctor supports.
@@ -125,7 +125,7 @@ public class AuditLogIngestActor : ReceiveActor
// InsertIfNotExistsAsync. The single-repository test ctor has no
// service provider — it falls through with no redactor, which preserves
// the small-payload assumptions baked into the existing fixtures.
// AuditLog-003: use CreateAsyncScope + await using so scoped EF Core
// Use CreateAsyncScope + await using so scoped EF Core
// services (IAsyncDisposable DbContexts) dispose asynchronously
// without blocking on sync Dispose() of pending connection cleanup.
if (_injectedRepository is not null)
@@ -135,7 +135,7 @@ public class AuditLogIngestActor : ReceiveActor
}
else
{
// AuditLog-014: guard scope-creation + repository resolution in a
// Guard scope-creation + repository resolution in a
// try/catch, mirroring OnCachedTelemetryAsync. A transient DI /
// DbContext-factory fault (pooled-context init, SQL-connection
// exhaustion, a resolution race during host churn) would otherwise
@@ -151,7 +151,7 @@ public class AuditLogIngestActor : ReceiveActor
await using var scope = _serviceProvider!.CreateAsyncScope();
var repository = scope.ServiceProvider.GetRequiredService<IAuditLogRepository>();
var redactor = scope.ServiceProvider.GetService<IAuditRedactor>();
// M6 Bundle E (T8): central health counter is best-effort —
// Central health counter is best-effort —
// unregistered (test composition roots) means the per-row catch
// simply logs without surfacing on the health dashboard.
var failureCounter = scope.ServiceProvider.GetService<ICentralAuditWriteFailureCounter>();
@@ -194,11 +194,11 @@ public class AuditLogIngestActor : ReceiveActor
// retry, reconciliation) is a silent no-op.
// Redact BEFORE the IngestedAtUtc stamp so the redacted
// copy carries the central-side ingest timestamp. The redactor
// is contract-bound to never throw. AuditLog-008: a null
// is contract-bound to never throw. A null
// redactor (test composition root, no IAuditRedactor
// registered) now falls back to the SafeDefault rather than
// pass-through, so HTTP header redaction always runs.
// C3 transitional shim: IngestedAtUtc is a DetailsJson field on
// IngestedAtUtc is a DetailsJson field on
// the canonical record, so stamp it via the projection helper.
var safeRedactor = redactor ?? SafeDefaultAuditRedactor.Instance;
var filtered = safeRedactor.Apply(evt);
@@ -210,7 +210,7 @@ public class AuditLogIngestActor : ReceiveActor
{
// Per-row catch — one bad row never sinks the whole batch.
// The row stays Pending at the site; the next drain retries.
// M6 Bundle E (T8): bump the central health counter so a
// Bump the central health counter so a
// sustained insert-throw failure surfaces on the dashboard.
try { failureCounter?.Increment(); }
catch { /* counter must never throw — defence in depth */ }
@@ -222,7 +222,7 @@ public class AuditLogIngestActor : ReceiveActor
}
/// <summary>
/// M3 dual-write handler. For every <see cref="CachedTelemetryEntry"/> the
/// Dual-write handler. For every <see cref="CachedTelemetryEntry"/> the
/// actor opens a fresh MS SQL transaction, inserts the AuditLog row
/// idempotently AND upserts the SiteCalls row monotonically. Both succeed
/// or both roll back, so the audit and operational mirrors never drift
@@ -248,13 +248,13 @@ public class AuditLogIngestActor : ReceiveActor
var auditRepo = scope.ServiceProvider.GetRequiredService<IAuditLogRepository>();
var siteCallRepo = scope.ServiceProvider.GetRequiredService<ISiteCallAuditRepository>();
var dbContext = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
// Bundle C (M5-T6): resolve the redactor for the whole batch from
// Resolve the redactor for the whole batch from
// the scope; null = SafeDefault for test composition roots that
// skip the redactor registration. The redactor is contract-bound to
// never throw, so we can apply it inside the per-entry try
// without risking an unbounded blast radius.
var redactor = scope.ServiceProvider.GetService<IAuditRedactor>();
// M6 Bundle E (T8): same best-effort central health counter as
// Same best-effort central health counter as
// the OnIngestAsync path — null on test composition roots that
// skip the registration.
var failureCounter = scope.ServiceProvider.GetService<ICentralAuditWriteFailureCounter>();
@@ -275,9 +275,9 @@ public class AuditLogIngestActor : ReceiveActor
// Redact the audit half BEFORE the dual-write — only the
// AuditLog row's payload columns are redactable; SiteCalls
// carries operational state only (status, retry count) and
// is left untouched. AuditLog-008: null redactor falls back
// is left untouched. Null redactor falls back
// to SafeDefault so header redaction always runs.
// C3 transitional shim: IngestedAtUtc is a DetailsJson field
// IngestedAtUtc is a DetailsJson field
// on the canonical record, so stamp it via the projection helper.
var safeRedactor = redactor ?? SafeDefaultAuditRedactor.Instance;
var filteredAudit = safeRedactor.Apply(entry.Audit);
@@ -298,7 +298,7 @@ public class AuditLogIngestActor : ReceiveActor
// EventId is NOT added to `accepted` so the site keeps its
// row Pending and retries on the next drain. Other entries
// in the batch continue with their own transactions.
// M6 Bundle E (T8): bump the central health counter so a
// Bump the central health counter so a
// sustained dual-write failure surfaces on the dashboard.
try { failureCounter?.Increment(); }
catch { /* counter must never throw — defence in depth */ }
@@ -2,7 +2,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Tuning knobs for the central
/// <see cref="AuditLogPartitionMaintenanceService"/> hosted service (M6-T5).
/// <see cref="AuditLogPartitionMaintenanceService"/> hosted service.
/// Defaults: once every 24 hours, keep at least one future monthly
/// boundary ahead of <see cref="DateTime.UtcNow"/>.
/// </summary>
@@ -7,7 +7,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Central <see cref="IHostedService"/> (M6-T5, Bundle D) that rolls
/// Central <see cref="IHostedService"/> that rolls
/// <c>pf_AuditLog_Month</c> forward once a day. Each tick opens a fresh DI
/// scope, resolves <see cref="IPartitionMaintenance"/>, and calls
/// <see cref="IPartitionMaintenance.EnsureLookaheadAsync"/> to SPLIT any
@@ -19,7 +19,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// </summary>
/// <remarks>
/// <para>
/// <b>Why a hosted service, not an actor.</b> Bundle C's
/// <b>Why a hosted service, not an actor.</b>
/// <see cref="AuditLogPurgeActor"/> sits inside the central singleton
/// because it needs supervised lifecycle alongside the rest of the
/// reconciliation / ingest pipeline. Roll-forward is genuinely a once-a-day
@@ -33,8 +33,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <b>Failure containment.</b> The tick body wraps the maintenance call in
/// a try/catch so a transient SQL Server error never tears down the hosted
/// service — the next tick simply retries. The exception is logged with
/// the original stack trace at <c>Error</c> level; ops surfaces (M6 Bundle
/// E's central health collector) can subscribe to the logger to alert on
/// the original stack trace at <c>Error</c> level; ops surfaces can subscribe to the logger to alert on
/// repeated failures.
/// </para>
/// <para>
@@ -9,7 +9,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Central singleton (M6 Bundle C) that drives the daily AuditLog partition
/// Central singleton that drives the daily AuditLog partition
/// purge. On a configurable timer (default 24 hours) the actor:
/// <list type="number">
/// <item>Queries <see cref="IAuditLogRepository.GetPartitionBoundariesOlderThanAsync"/>
@@ -19,7 +19,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <see cref="IAuditLogRepository.SwitchOutPartitionAsync"/> which runs
/// the drop-and-rebuild dance around <c>UX_AuditLog_EventId</c>.</item>
/// <item>Publishes <see cref="AuditLogPurgedEvent"/> on the actor-system
/// EventStream so the Bundle E central health collector + ops surfaces
/// EventStream so the central health collector + ops surfaces
/// can subscribe without coupling to this actor.</item>
/// </list>
/// </summary>
@@ -51,8 +51,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <para>
/// <b>EventStream.</b> Publishing <see cref="AuditLogPurgedEvent"/> through
/// the EventStream rather than direct messaging avoids coupling this actor
/// to its consumers; M6 Bundle E will subscribe a central health-counter
/// bridge that surfaces purge progress on the central health report.
/// to its consumers; a central health-counter bridge will subscribe to
/// surface purge progress on the central health report.
/// </para>
/// </remarks>
public class AuditLogPurgeActor : ReceiveActor
@@ -134,7 +134,7 @@ public class AuditLogPurgeActor : ReceiveActor
// restart.
var threshold = DateTime.UtcNow - TimeSpan.FromDays(_auditOptions.RetentionDays);
// AuditLog-003: use CreateAsyncScope + await using so scoped EF Core
// Use CreateAsyncScope + await using so scoped EF Core
// services (IAsyncDisposable DbContexts) dispose asynchronously
// without blocking on sync Dispose() of pending connection cleanup.
await using var scope = _services.CreateAsyncScope();
@@ -206,7 +206,7 @@ public class AuditLogPurgeActor : ReceiveActor
}
}
// M5.5 (T3): after the channel-blind global partition switch-out, apply any
// After the channel-blind global partition switch-out, apply any
// per-channel retention overrides that are SHORTER than the global window via
// a bounded, batched row DELETE on the same maintenance path. The global
// switch-out has already dropped whole months older than RetentionDays; these
@@ -216,7 +216,7 @@ public class AuditLogPurgeActor : ReceiveActor
}
/// <summary>
/// M5.5 (T3): runs each per-channel retention override whose window is strictly
/// Runs each per-channel retention override whose window is strictly
/// shorter than the global <see cref="AuditLogOptions.RetentionDays"/>, deleting
/// rows of that channel older than the channel-specific threshold via a bounded,
/// batched maintenance-path DELETE. Each channel runs inside its own try/catch so
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Tuning knobs for the central <see cref="AuditLogPurgeActor"/> singleton.
/// Default cadence is 24 hours per the M6 plan; the retention window itself
/// Default cadence is 24 hours; the retention window itself
/// is sourced from <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Configuration.AuditLogOptions.RetentionDays"/>
/// (default 365) so operators tune retention from a single section.
/// </summary>
@@ -31,7 +31,7 @@ public sealed class AuditLogPurgeOptions
public int IntervalHours { get; set; } = 24;
/// <summary>
/// M5.5 (T3): batch size for the per-channel retention-override row DELETE
/// Batch size for the per-channel retention-override row DELETE
/// (<see cref="ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories.IAuditLogRepository.PurgeChannelOlderThanAsync"/>).
/// Each <c>DELETE TOP (@batch)</c> caps the transaction-log and lock footprint
/// per statement; the repository loops batches until no rows remain. Default
@@ -40,7 +40,7 @@ public sealed class AuditLogPurgeOptions
/// <see cref="ChannelPurgeBatchSize"/>.
/// </summary>
/// <remarks>
/// AuditLog-013: the operator-facing config key is <c>ChannelPurgeBatchSize</c>
/// The operator-facing config key is <c>ChannelPurgeBatchSize</c>
/// (per Component-AuditLog.md), so the binder maps that documented key onto this
/// backing property via <see cref="ConfigurationKeyNameAttribute"/>. The unattributed
/// property name (<c>ChannelPurgeBatchSizeConfigured</c>) would otherwise have been
@@ -2,8 +2,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Published on the actor-system EventStream by <see cref="AuditLogPurgeActor"/>
/// after each successful partition switch-out. Downstream consumers (Bundle E
/// central health collector, ops dashboards, audit trails) subscribe so a
/// after each successful partition switch-out. Downstream consumers (central
/// health collector, ops dashboards, audit trails) subscribe so a
/// purge action is observable without the actor needing to know about any
/// specific subscriber.
/// </summary>
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E (T9) — bridges
/// Bridges
/// <see cref="IAuditRedactionFailureCounter"/> (incremented by
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/> every time
/// a header / body / SQL parameter redactor stage throws and the redactor has
@@ -13,11 +13,11 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// </summary>
/// <remarks>
/// <para>
/// <b>Site vs central.</b> M5 Bundle C wired the SITE-side bridge
/// <b>Site vs central.</b> Wired the SITE-side bridge
/// (<see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Site.HealthMetricsAuditRedactionFailureCounter"/>),
/// which routes increments into the site health report payload's
/// <c>AuditRedactionFailure</c> field. That handles redactor failures on the
/// site SQLite hot-path (FallbackAuditWriter). M6 Bundle E (T9) adds the
/// site SQLite hot-path (FallbackAuditWriter). Adds the
/// MIRROR bridge here so the same payload filter — when it runs on the
/// central <see cref="CentralAuditWriter"/> /
/// <see cref="AuditLogIngestActor"/> paths — surfaces its failures on the
@@ -47,22 +47,22 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
private readonly INodeIdentityProvider? _nodeIdentity;
/// <summary>
/// Bundle C (M5-T6) — the central direct-write path used by the
/// The central direct-write path used by the
/// NotificationOutboxActor dispatch and the Inbound API middleware also
/// needs to truncate + redact before the row hits MS SQL. The filter is
/// optional so the M4 test composition roots that don't pass one keep
/// optional so test composition roots that don't pass one keep
/// working (they only ever write small payloads); production DI registers
/// the real filter via <see cref="ServiceCollectionExtensions.AddAuditLog"/>.
/// M6 Bundle E (T8) — adds the optional
/// Adds the optional
/// <see cref="ICentralAuditWriteFailureCounter"/> so a swallowed repository
/// throw bumps the central health surface's
/// <c>CentralAuditWriteFailures</c> counter. Defaults to a NoOp so test
/// composition roots that don't wire the counter keep their current
/// behaviour. SourceNode-stamping (Task 12) — adds the optional
/// behaviour. SourceNode-stamping — adds the optional
/// <see cref="INodeIdentityProvider"/> so central-origin rows (Notification
/// Outbox dispatch, Inbound API) carry the writing central node's
/// identifier when the caller hasn't already supplied one. Optional /
/// defaulting-to-null so M4 test composition roots that don't pass a
/// defaulting-to-null so test composition roots that don't pass a
/// provider keep working — the caller-wins discipline means an absent
/// provider simply leaves SourceNode at whatever the caller set (often
/// null, which is the legacy behaviour).
@@ -81,8 +81,8 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// AuditLog-008: never default to null — over-redact instead.
// C3 (Task 2.5): wired via the canonical IAuditRedactor seam.
// Never default to null — over-redact instead.
// Wired via the canonical IAuditRedactor seam.
// SafeDefaultAuditRedactor applies HTTP header redaction with
// hard-coded sensitive defaults so a composition root that omits the
// real redactor still scrubs Authorization / X-Api-Key / Cookie /
@@ -106,13 +106,13 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
try
{
// Redact BEFORE stamping IngestedAtUtc + handing to the repo. The
// redactor contract is "never throws". AuditLog-008: _redactor is
// redactor contract is "never throws". _redactor is
// now non-null (SafeDefaultAuditRedactor fallback) so header
// redaction always runs even in composition roots that omit the
// real redactor.
var filtered = _redactor.Apply(evt);
// SourceNode-stamping (Task 12): caller-provided value wins
// SourceNode-stamping: caller-provided value wins
// (supports any future direct-write callsite that already has its
// own node id); otherwise stamp from the local
// INodeIdentityProvider, when one is wired. Production DI on
@@ -126,7 +126,7 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
await using var scope = _services.CreateAsyncScope();
var repo = scope.ServiceProvider.GetRequiredService<IAuditLogRepository>();
// C3 transitional shim: IngestedAtUtc is a DetailsJson field on the
// IngestedAtUtc is a DetailsJson field on the
// canonical record, so stamp it via the projection helper.
var stamped = AuditRowProjection.WithIngestedAtUtc(filtered, DateTime.UtcNow);
await repo.InsertIfNotExistsAsync(stamped, ct).ConfigureAwait(false);
@@ -134,7 +134,7 @@ public sealed class CentralAuditWriter : ICentralAuditWriter
catch (Exception ex)
{
// Audit failure NEVER aborts the user-facing action — swallow and log.
// M6 Bundle E (T8): also surface the failure on the central health
// Also surface the failure on the central health
// counter so a sustained audit-write outage is visible on the
// health dashboard rather than disappearing into the log file.
try
@@ -12,7 +12,7 @@ using PullAuditEventsResponse = ZB.MOM.WW.ScadaBridge.Commons.Messages.Integrati
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Production <see cref="IPullAuditEventsClient"/> (Audit Log #23, M6) that the
/// Production <see cref="IPullAuditEventsClient"/> that the
/// central <see cref="SiteAuditReconciliationActor"/> uses to pull the next
/// reconciliation batch from a site over the <c>PullAuditEvents</c> unary gRPC
/// RPC served by <c>SiteStreamGrpcServer</c>.
@@ -13,7 +13,7 @@ using PullSiteCallsResponse = ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Production <see cref="IPullSiteCallsClient"/> (Site Call Audit #22) that the
/// Production <see cref="IPullSiteCallsClient"/> (Site Call Audit) 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>.
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E read-side surface exposing the central-side
/// Read-side surface exposing the central-side
/// audit-health counters: <see cref="CentralAuditWriteFailures"/> (every
/// repository insert throw from <see cref="CentralAuditWriter"/> /
/// <see cref="AuditLogIngestActor"/>), <see cref="AuditRedactionFailure"/>
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <para>
/// <b>Read-only contract.</b> Implementations expose a point-in-time snapshot
/// — increments and tracker updates happen through the dedicated counter /
/// tracker interfaces, not through this surface. Consumers (M7+ central
/// tracker interfaces, not through this surface. Consumers (central
/// health pages) read these properties; they never mutate.
/// </para>
/// <para>
@@ -25,7 +25,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// failure / redaction-failure counters originate ON central (no site report
/// carries them), so they live on a dedicated snapshot rather than being
/// retro-fitted into a per-site state. The two surfaces will be composed at
/// the M7 dashboard layer.
/// the dashboard layer.
/// </para>
/// </remarks>
public interface IAuditCentralHealthSnapshot
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M5.3 (T7) counter sink incremented by
/// Audit Log counter sink incremented by
/// <see cref="ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware.AuditWriteMiddleware"/>
/// whenever an inbound request or response body is truncated at the
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Configuration.AuditLogOptions.InboundMaxBytes"/>
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E (T8) counter sink invoked by central-side audit
/// Counter sink invoked by central-side audit
/// writers (<see cref="CentralAuditWriter"/>, <see cref="AuditLogIngestActor"/>)
/// every time a repository <c>InsertIfNotExistsAsync</c> throws. Mirrors the
/// site-side <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Site.IAuditWriteFailureCounter"/>
@@ -14,7 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// instead. A NoOp default is the correct safe fallback while the central
/// health surface is being wired in; <see cref="AuditCentralHealthSnapshot"/>
/// is the production binding that routes increments into the aggregated
/// central health snapshot consumed by future M7+ pages.
/// central health snapshot, to be consumed by future central health pages.
/// </remarks>
public interface ICentralAuditWriteFailureCounter
{
@@ -4,7 +4,7 @@ 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
/// surface used by the Site Call Audit 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.
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Central singleton (M6 Bundle B) that drives the audit-log reconciliation
/// Central singleton that drives the audit-log reconciliation
/// pull loop. On a configurable timer (default 5 minutes) the actor walks every
/// known site, asks the site for any <see cref="AuditEvent"/> rows with
/// <see cref="AuditEvent.OccurredAtUtc"/> &gt;= the site's last reconciled
@@ -23,7 +23,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// missed push (gRPC blip, central restart, site offline) is eventually
/// repaired by central re-pulling whatever the site still has in
/// <c>Pending</c>/<c>Forwarded</c> state. Idempotency on
/// <see cref="AuditEvent.EventId"/> (M2 Bundle A's race-fix) makes duplicate
/// <see cref="AuditEvent.EventId"/> makes duplicate
/// arrivals from both paths a silent no-op.
/// </para>
/// <para>
@@ -33,7 +33,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// the cursors reset to <see cref="DateTime.MinValue"/>. That is conservative
/// but correct — the next tick simply asks for everything the site still has,
/// and idempotent ingest swallows the dupes. Persisting cursors to MS SQL was
/// considered and rejected for M6: the cost of a write per tick outweighs the
/// considered and rejected: the cost of a write per tick outweighs the
/// rare benefit of avoiding one over-broad pull after a restart.
/// </para>
/// <para>
@@ -41,7 +41,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// consecutive pull cycles BOTH return non-empty AND <c>MoreAvailable=true</c>
/// — i.e. the backlog isn't draining. The actor publishes
/// <see cref="SiteAuditTelemetryStalledChanged"/> on the actor system's
/// EventStream so a future <c>ICentralHealthCollector</c> bridge (M6 Bundle E)
/// EventStream so a future <c>ICentralHealthCollector</c> bridge
/// can flip the health metric without coupling this actor to the health
/// collection surface today.
/// </para>
@@ -97,7 +97,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
private readonly Dictionary<string, bool> _stalled = new();
/// <summary>
/// AuditLog-004: per-EventId retry counter for rows whose central insert
/// Per-EventId retry counter for rows whose central insert
/// threw. While a row keeps failing AND is below
/// <see cref="MaxPermanentInsertAttempts"/>, the cursor is held back so the
/// next reconciliation tick re-pulls and retries the row. Crossing the
@@ -110,7 +110,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
private readonly Dictionary<Guid, int> _failedInsertAttempts = new();
/// <summary>
/// AuditLog-004: number of consecutive central-insert failures before a row
/// Number of consecutive central-insert failures before a row
/// is permanently abandoned with a Critical log entry and the cursor is
/// allowed to advance past it. Five attempts at the 5-minute default tick
/// is ~25 min of retry budget before a stuck row stops blocking progress.
@@ -199,7 +199,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
return;
}
// AuditLog-003: use CreateAsyncScope + await using so scoped EF Core
// Use CreateAsyncScope + await using so scoped EF Core
// services (IAsyncDisposable DbContexts) dispose asynchronously
// without blocking on sync Dispose() of pending connection cleanup.
await using var scope = _services.CreateAsyncScope();
@@ -260,9 +260,8 @@ public class SiteAuditReconciliationActor : ReceiveActor
{
// Idempotent repository write: duplicate EventIds (from a
// concurrent push, or a retry of this very pull) collapse to
// a no-op courtesy of M2 Bundle A's race-fix on
// InsertIfNotExistsAsync.
// C3: IngestedAtUtc is a DetailsJson field on the canonical record —
// a no-op on InsertIfNotExistsAsync.
// IngestedAtUtc is a DetailsJson field on the canonical record —
// stamp it via the projection helper.
var ingested = AuditRowProjection.WithIngestedAtUtc(evt, nowUtc);
await repository.InsertIfNotExistsAsync(ingested).ConfigureAwait(false);
@@ -271,7 +270,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
}
catch (Exception ex)
{
// AuditLog-004: per-row catch so one bad event does not abandon
// Per-row catch so one bad event does not abandon
// the rest of the batch. Track the failure count per EventId —
// below MaxPermanentInsertAttempts the cursor is HELD BACK so
// the next tick re-pulls and retries; at the threshold the row
@@ -305,7 +304,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
}
}
// C3: canonical OccurredAtUtc is a DateTimeOffset; the cursor is a UTC DateTime.
// Canonical OccurredAtUtc is a DateTimeOffset; the cursor is a UTC DateTime.
var occurredUtc = evt.OccurredAtUtc.UtcDateTime;
if (advanceForThisRow && occurredUtc > maxOccurred)
{
@@ -313,7 +312,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
}
}
// AuditLog-004: only advance the persisted cursor if no event in this
// Only advance the persisted cursor if no event in this
// batch is still being retried. Leaving the cursor at `since` re-pulls
// the whole batch next tick — successful rows are no-ops thanks to
// InsertIfNotExistsAsync's idempotency, and the failing row gets
@@ -388,7 +387,7 @@ public class SiteAuditReconciliationActor : ReceiveActor
/// <summary>
/// Published on the actor system EventStream when a site's reconciliation
/// puller transitions into or out of the "stalled" state (backlog not
/// draining across multiple cycles). The M6 Bundle E central health collector
/// draining across multiple cycles). The central health collector
/// will subscribe to this and surface
/// <c>SiteAuditTelemetryStalled</c> on the health-report payload.
/// </summary>
@@ -2,13 +2,13 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Tuning knobs for the central <see cref="SiteAuditReconciliationActor"/> singleton.
/// Defaults mirror the M6 Bundle B brief: pull every 5 minutes per site, 256 rows per
/// Defaults: pull every 5 minutes per site, 256 rows per
/// batch, declare a site "stalled" after two consecutive pull cycles return non-empty
/// AND <c>MoreAvailable=true</c> (the backlog is not draining).
/// </summary>
/// <remarks>
/// <para>
/// Per the M6 plan the reconciliation actor is the fallback when push telemetry is
/// The reconciliation actor is the fallback when push telemetry is
/// lost; it is intentionally low-frequency. Lowering
/// <see cref="ReconciliationIntervalSeconds"/> in production trades MS SQL load for
/// fresher self-healing — keep the default unless a deployment can prove the extra
@@ -5,10 +5,10 @@ using Akka.Event;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Audit Log (#23) M6 Bundle E (T7) — central singleton that subscribes to the
/// Central singleton that subscribes to the
/// actor system's EventStream for <see cref="SiteAuditTelemetryStalledChanged"/>
/// publications and maintains a per-site latched stalled-state map readable
/// via <see cref="Snapshot"/>. Consumed by the M6 Bundle E
/// via <see cref="Snapshot"/>. Consumed by the
/// <see cref="AuditCentralHealthSnapshot"/> aggregator so the central health
/// surface can surface per-site "reconciliation isn't draining" without
/// coupling the publisher (<see cref="SiteAuditReconciliationActor"/>) to the