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
@@ -45,8 +45,8 @@ public class DefaultSiteClientFactory : ISiteClientFactory
/// Resolves site addresses from the database on a periodic refresh cycle and manages
/// per-site ClusterClient instances.
///
/// WP-4: All 8 message patterns routed through this actor.
/// WP-5: Ask timeout on connection drop (no central buffering). Debug streams killed on interruption.
/// All 8 message patterns routed through this actor.
/// Ask timeout on connection drop (no central buffering). Debug streams killed on interruption.
/// </summary>
public class CentralCommunicationActor : ReceiveActor
{
@@ -61,7 +61,7 @@ public class CentralCommunicationActor : ReceiveActor
/// </summary>
private readonly Dictionary<string, (IActorRef Client, ImmutableHashSet<string> ContactAddresses)> _siteClients = new();
// Communication-016: the previous _debugSubscriptions / _inProgressDeployments
// The previous _debugSubscriptions / _inProgressDeployments
// dictionaries existed solely to support a documented "synchronous kill streams +
// mark deployments failed on site disconnect" workflow triggered by
// ConnectionStateChanged. No production code ever emitted that message — only
@@ -77,7 +77,7 @@ public class CentralCommunicationActor : ReceiveActor
private ICancelable? _refreshSchedule;
/// <summary>
/// Communication-019: per-actor lifecycle CTS threaded into the periodic
/// Per-actor lifecycle CTS threaded into the periodic
/// <see cref="LoadSiteAddressesFromDb"/> repository call so a hung MS SQL
/// connection is bounded by actor shutdown rather than holding piped tasks
/// open indefinitely. Cancelled in <see cref="PostStop"/>; never reset.
@@ -160,7 +160,7 @@ public class CentralCommunicationActor : ReceiveActor
// Periodic refresh trigger
Receive<RefreshSiteAddresses>(_ => LoadSiteAddressesFromDb());
// Communication-006: a faulted LoadSiteAddressesFromDb task is piped here as a
// A faulted LoadSiteAddressesFromDb task is piped here as a
// Status.Failure. Without this handler the failure was an unhandled message
// (debug-level only) and the refresh failed silently — operators could not
// distinguish "no sites configured" from "database is down". Log at Warning.
@@ -196,7 +196,7 @@ public class CentralCommunicationActor : ReceiveActor
// so the NotificationStatusResponse routes back to the querying site.
Receive<NotificationStatusQuery>(HandleNotificationStatusQuery);
// Audit Log (#23): the Host registers the AuditLogIngestActor singleton
// Audit Log: the Host registers the AuditLogIngestActor singleton
// proxy after this actor is created (the proxy cannot exist before this
// actor's construction).
Receive<RegisterAuditIngest>(msg =>
@@ -205,13 +205,13 @@ public class CentralCommunicationActor : ReceiveActor
_log.Info("Registered audit ingest proxy");
});
// Audit Log (#23) site→central ingest: a site forwards a batch of audit
// Audit Log site→central ingest: a site forwards a batch of audit
// events to the central cluster via ClusterClient. Ask the ingest proxy
// and pipe the IngestAuditEventsReply back to the original Sender (the
// site's ClusterClient path) so the site can flip its rows to Forwarded.
Receive<IngestAuditEventsCommand>(HandleIngestAuditEvents);
// Audit Log (#23 M3) combined-telemetry ingest: routes to the same proxy
// Audit Log combined-telemetry ingest: routes to the same proxy
// the same way; the proxy replies with an IngestCachedTelemetryReply.
Receive<IngestCachedTelemetryCommand>(HandleIngestCachedTelemetry);
@@ -322,7 +322,7 @@ public class CentralCommunicationActor : ReceiveActor
// Capture Sender before the async/PipeTo — Akka resets Sender between dispatches.
var replyTo = Sender;
// Bound the DB work by the actor lifecycle (Communication-019). The CTS may have
// Bound the DB work by the actor lifecycle. The CTS may have
// been disposed by PostStop on a racing late message; treat that as "actor gone".
CancellationToken ct;
try
@@ -390,7 +390,7 @@ public class CentralCommunicationActor : ReceiveActor
}
}
// Communication-016: HandleConnectionStateChanged removed — no production
// HandleConnectionStateChanged removed — no production
// caller emitted ConnectionStateChanged, so the workflow ran only in tests.
// Disconnect detection is owned by the transport layers (gRPC keepalive +
// ClusterClient/Ask timeout).
@@ -402,7 +402,7 @@ public class CentralCommunicationActor : ReceiveActor
_log.Warning("No ClusterClient for site {0}, cannot route message {1}",
envelope.SiteId, envelope.Message.GetType().Name);
// The Ask will timeout on the caller side — no central buffering (WP-5)
// The Ask will timeout on the caller side — no central buffering
return;
}
@@ -415,7 +415,7 @@ public class CentralCommunicationActor : ReceiveActor
private void LoadSiteAddressesFromDb()
{
var self = Self;
// Communication-019: pass the actor's lifecycle CT into the repository
// Pass the actor's lifecycle CT into the repository
// call so a hung database query is cancelled when the actor stops
// rather than leaving the piped task to accumulate. Captured locally
// because the lifecycle CTS may have been disposed by PostStop on a
@@ -459,7 +459,7 @@ public class CentralCommunicationActor : ReceiveActor
contacts[site.SiteIdentifier] = addrs;
}
// Communication-020: freeze the cross-task payload before piping to
// Freeze the cross-task payload before piping to
// Self. The message record exposes read-only types (
// IReadOnlyDictionary / IReadOnlyList) so the Akka.NET message-
// immutability convention is enforced by type, not just convention.
@@ -486,7 +486,7 @@ public class CentralCommunicationActor : ReceiveActor
// Add or update
foreach (var (siteId, addresses) in msg.SiteContacts)
{
// Communication-009: parse all addresses up front inside a try/catch so a
// Parse all addresses up front inside a try/catch so a
// single malformed site row cannot abort the whole refresh loop and leave
// the cache half-updated. A bad site is logged and skipped; others proceed.
ImmutableHashSet<ActorPath> contactPaths;
@@ -525,7 +525,7 @@ public class CentralCommunicationActor : ReceiveActor
_log.Info("Site ClusterClient cache refreshed with {0} site(s)", _siteClients.Count);
}
// Communication-016: TrackMessageForCleanup removed — the dicts it fed
// TrackMessageForCleanup removed — the dicts it fed
// existed solely to support the dead ConnectionStateChanged workflow.
/// <inheritdoc />
@@ -574,7 +574,7 @@ public class CentralCommunicationActor : ReceiveActor
{
_log.Info("CentralCommunicationActor stopped");
_refreshSchedule?.Cancel();
// Communication-019: cancel any in-flight LoadSiteAddressesFromDb so a
// Cancel any in-flight LoadSiteAddressesFromDb so a
// hung MS SQL query does not outlive the actor.
try
{
@@ -597,7 +597,7 @@ public record RefreshSiteAddresses;
/// Internal message carrying the loaded site contact data from the database.
/// ClusterClient creation happens on the actor thread in HandleSiteAddressCacheLoaded.
///
/// Communication-020: the payload is exposed as <see cref="IReadOnlyDictionary{TKey,TValue}"/>
/// The payload is exposed as <see cref="IReadOnlyDictionary{TKey,TValue}"/>
/// of <see cref="IReadOnlyList{T}"/> so the Akka.NET "messages are immutable"
/// convention is enforced at the type level rather than relying on producer
/// discipline. The producer wraps the constructed buckets with
@@ -607,7 +607,7 @@ internal record SiteAddressCacheLoaded(IReadOnlyDictionary<string, IReadOnlyList
/// <summary>
/// Notification sent to debug view subscribers when the stream is terminated
/// due to site disconnection (WP-5).
/// due to site disconnection.
/// </summary>
public record DebugStreamTerminated(string SiteId, string CorrelationId);
@@ -11,7 +11,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Actors;
/// are session-based and temporary — this actor holds no persisted state and does not
/// derive from an Akka.Persistence base class; its state does not survive a restart.
/// <para>
/// <b>Stream-first lifecycle (M2.18, #26).</b> To avoid losing any
/// <b>Stream-first lifecycle.</b> To avoid losing any
/// <see cref="AttributeValueChanged"/>/<see cref="AlarmStateChanged"/> that occurs on
/// the site during the snapshot-build + network-transit window, the gRPC server-streaming
/// subscription is opened FIRST (in <see cref="PreStart"/>), alongside the
@@ -51,7 +51,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
/// <summary>
/// How long a freshly-opened gRPC stream must stay up before its retry budget
/// is considered "recovered" and <see cref="_retryCount"/> is reset to 0.
/// Communication-008: the retry count must NOT be reset by individual events —
/// The retry count must NOT be reset by individual events —
/// a stream that connects, delivers one event, then fails repeatedly would
/// otherwise reconnect forever and never trip <see cref="MaxRetries"/>. Resetting
/// only after a stable interval bounds a flapping stream.
@@ -64,7 +64,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
private CancellationTokenSource? _grpcCts;
/// <summary>
/// Phase flag (M2.18). <see langword="false"/> until the initial
/// Phase flag. <see langword="false"/> until the initial
/// <see cref="DebugViewSnapshot"/> has been delivered and the pre-snapshot buffer
/// flushed; <see langword="true"/> thereafter (pass-through). Mutated only on the
/// actor thread. A reconnect does NOT touch this flag — a mid-session reconnect
@@ -125,8 +125,8 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
_grpcNodeBAddress = grpcNodeBAddress;
// Initial snapshot response from the site (via ClusterClient).
// M2.11: if the site reports InstanceNotFound=true the instance is not
// deployed there. M2.18: under the stream-first lifecycle the gRPC stream
// If the site reports InstanceNotFound=true the instance is not
// deployed there. Under the stream-first lifecycle the gRPC stream
// was already opened in PreStart, so the not-found path must tear it down
// (CleanupGrpc) rather than enter pass-through. Forward the snapshot (with
// InstanceNotFound=true) to _onEvent so DebugStreamService's TCS resolves and
@@ -147,7 +147,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
{
_log.Warning("Instance {0} is not deployed on site; terminating debug stream",
_instanceUniqueName);
// M2.18: the stream-first subscription opened in PreStart is for a
// The stream-first subscription opened in PreStart is for a
// non-deployed instance — cancel it (and any buffered gap events are
// discarded with the actor). No pass-through.
// _stopped is set AFTER CleanupGrpc() to match the ordering in the
@@ -176,11 +176,11 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
});
// Domain events arriving via Self.Tell from gRPC callback.
// Communication-008: receiving an event must NOT reset _retryCount — a
// Receiving an event must NOT reset _retryCount — a
// flapping stream that delivers a single event between failures would
// otherwise never trip MaxRetries. The retry budget is recovered only by
// GrpcStreamStable (a stream that has stayed up for StabilityWindow).
// M2.18: before the snapshot has been delivered, BUFFER (in arrival order)
// Before the snapshot has been delivered, BUFFER (in arrival order)
// rather than deliver — these may be gap-window events. After the snapshot has
// been flushed, pass through directly (same handler, phase-dependent behavior).
Receive<AttributeValueChanged>(changed => HandleStreamEvent(changed));
@@ -268,7 +268,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
/// <summary>
/// Flushes the pre-snapshot buffer in arrival order, deduping each event against the
/// just-delivered snapshot (M2.18).
/// just-delivered snapshot.
/// <para>
/// <b>Dedup rule.</b> Identity is per-entity:
/// attributes by (InstanceUniqueName, AttributePath, AttributeName); alarms by
@@ -389,7 +389,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
{
_log.Info("Starting debug stream bridge for {0} on site {1}", _instanceUniqueName, _siteIdentifier);
// M2.18 stream-first: open the gRPC live-event subscription BEFORE (and
// Stream-first: open the gRPC live-event subscription BEFORE (and
// alongside) requesting the snapshot, so events occurring during the
// snapshot-build + network-transit window are captured (buffered) and not lost.
OpenGrpcStream();
@@ -421,7 +421,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
_grpcCts = new CancellationTokenSource();
// Arm the stability timer: if the stream stays up for StabilityWindow the
// retry budget is recovered (Communication-008). Cancelled by HandleGrpcError.
// retry budget is recovered. Cancelled by HandleGrpcError.
Timers.StartSingleTimer(StabilityTimerKey, new GrpcStreamStable(), StabilityWindow);
var client = _grpcFactory.GetOrCreate(_siteIdentifier, endpoint);
@@ -445,7 +445,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
if (_stopped) return;
// The stream failed before reaching the stability window — its retry
// budget is NOT recovered (Communication-008).
// budget is NOT recovered.
Timers.Cancel(StabilityTimerKey);
_retryCount++;
@@ -21,7 +21,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Actors;
/// them to the appropriate local actors. Also sends heartbeats and health reports
/// to central via the registered ClusterClient.
///
/// WP-4: Routes all 8 message patterns to local handlers.
/// Routes all 8 message patterns to local handlers.
/// </summary>
public class SiteCommunicationActor : ReceiveActor, IWithTimers
{
@@ -30,7 +30,7 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
private readonly CommunicationOptions _options;
/// <summary>
/// Communication-018: predicate that returns <c>true</c> when this node is
/// Predicate that returns <c>true</c> when this node is
/// the active member of the local site cluster (used to stamp
/// <see cref="HeartbeatMessage.IsActive"/>). Production builds default to
/// the Akka <see cref="Cluster"/> leader check; tests inject a stub so they
@@ -66,7 +66,7 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
/// <param name="options">Communication options including heartbeat interval and transport settings.</param>
/// <param name="deploymentManagerProxy">Local reference to the Deployment Manager singleton proxy.</param>
/// <param name="isActiveCheck">
/// Communication-018: optional override returning <c>true</c> when this node
/// Optional override returning <c>true</c> when this node
/// is the active member of the site cluster. <c>null</c> uses the real
/// Akka <see cref="Cluster"/> leader check (the default for production
/// wiring); tests pass a stub so they do not need to load Akka.Cluster
@@ -103,7 +103,7 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
Receive<EnableInstanceCommand>(msg => _deploymentManagerProxy.Forward(msg));
Receive<DeleteInstanceCommand>(msg => _deploymentManagerProxy.Forward(msg));
// DeploymentManager-006: query-the-site-before-redeploy — forward to
// Query-the-site-before-redeploy — forward to
// the Deployment Manager, which owns the deployed-config store and
// answers with the instance's currently-applied deployment identity.
Receive<DeploymentStateQueryRequest>(msg => _deploymentManagerProxy.Forward(msg));
@@ -161,8 +161,8 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
// children holding the live OPC UA sessions.
Receive<ReadTagValuesCommand>(msg => _deploymentManagerProxy.Forward(msg));
// OPC UA tag-picker address-space search (T15) and secured-write execute
// (T14) — same singleton routing rationale as BrowseNodeCommand above: the
// OPC UA tag-picker address-space search and secured-write execute
// — same singleton routing rationale as BrowseNodeCommand above: the
// DataConnectionActor children that own the live OPC UA sessions exist only
// on the singleton's (active) node, so these must hop through the Deployment
// Manager proxy too. Without these forwards the commands dead-letter and the
@@ -171,13 +171,13 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
Receive<SearchAddressSpaceCommand>(msg => _deploymentManagerProxy.Forward(msg));
Receive<Commons.Messages.DataConnection.WriteTagRequest>(msg => _deploymentManagerProxy.Forward(msg));
// OPC UA endpoint Verify (T17) — probes a (possibly unsaved) endpoint config
// OPC UA endpoint Verify — probes a (possibly unsaved) endpoint config
// WITHOUT persisting it. The Deployment Manager singleton's dcl-manager runs
// the probe directly (no existing connection required), so — like the
// commands above — Verify routes through the singleton's active node.
Receive<VerifyEndpointCommand>(msg => _deploymentManagerProxy.Forward(msg));
// OPC UA server-certificate trust management (T17 / D6) — forward to the
// OPC UA server-certificate trust management — forward to the
// Deployment Manager singleton, which owns the cross-node trust broadcast.
// The trusted-peer PKI store is node-wide per site node, so a trust/remove
// decision must reach BOTH nodes' CertStoreActor; the singleton broadcasts
@@ -236,7 +236,7 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
}
});
// Task 5 (#22): central→site Retry/Discard relay for parked cached
// Central→site Retry/Discard relay for parked cached
// operations. SiteCallAuditActor relays these over the command/control
// channel; the parked-message handler executes them against the local
// S&F buffer and replies a ParkedOperationActionAck that routes back to
@@ -311,7 +311,7 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
new ClusterClient.Send("/user/central-communication", msg), Sender);
});
// Audit Log (#23): forward a batch of site-local audit events to the
// Audit Log: forward a batch of site-local audit events to the
// central cluster. The site SiteAuditTelemetryActor drains its SQLite
// Pending queue through the ClusterClientSiteAuditClient, which Asks
// this actor; the original Sender (that Ask) is passed as the
@@ -338,7 +338,7 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
new ClusterClient.Send("/user/central-communication", msg), Sender);
});
// Audit Log (#23) M3: forward a batch of combined cached-call telemetry
// Audit Log: forward a batch of combined cached-call telemetry
// packets to the central cluster. Same forward + reply-routing pattern
// as IngestAuditEventsCommand; central replies with an
// IngestCachedTelemetryReply.
@@ -359,7 +359,7 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
new ClusterClient.Send("/user/central-communication", msg), Sender);
});
// Site startup reconciliation (Task 18): forward the node's local-inventory
// Site startup reconciliation: forward the node's local-inventory
// ReconcileSiteRequest to the central cluster. The original Sender (the
// SiteReconciliationActor's Ask) is passed as the ClusterClient.Send sender so
// the ReconcileSiteResponse routes straight back to the waiting Ask, not here.
@@ -453,7 +453,7 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
var hostname = Environment.MachineName;
// Communication-018: stamp HeartbeatMessage.IsActive with this node's
// Stamp HeartbeatMessage.IsActive with this node's
// true active/standby role rather than hard-coding `true`. The field is
// part of the wire contract (additive-only-evolution) so a future
// central health dashboard can distinguish "active node down, standby
@@ -487,7 +487,7 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
}
/// <summary>
/// Communication-018: default active-node check used when no override is
/// Default active-node check used when no override is
/// supplied. Mirrors <c>ActiveNodeGate</c> in the Host (and
/// <c>ActiveNodeHealthCheck</c>): the node is the active member of the
/// site cluster when it is the current cluster leader AND its own
@@ -64,7 +64,7 @@ public class CommunicationService
}
/// <summary>
/// Sets the Site Call Audit (#22) singleton proxy reference. Called during
/// Sets the Site Call Audit singleton proxy reference. Called during
/// actor system startup. The Site Call Audit actor is central-local, so Site
/// Calls read calls Ask this proxy directly (no SiteEnvelope routing), the
/// same pattern as <see cref="SetNotificationOutbox"/>.
@@ -136,7 +136,7 @@ public class CommunicationService
}
/// <summary>
/// DeploymentManager-006: queries a site for the currently-applied deployment
/// Queries a site for the currently-applied deployment
/// identity of a single instance. Used by the Deployment Manager before a
/// re-deploy to reconcile against the site's actual state. Sent over the
/// existing ClusterClient command/control transport; the Ask times out (no
@@ -417,7 +417,7 @@ public class CommunicationService
envelope, _options.QueryTimeout, cancellationToken);
}
// ── OPC UA Server-Certificate Trust (T17 / D6 — node-wide site PKI store) ──
// ── OPC UA Server-Certificate Trust (node-wide site PKI store) ──
/// <summary>
/// Asks the owning site to trust an OPC UA server certificate at every site
@@ -508,7 +508,7 @@ public class CommunicationService
envelope, _options.QueryTimeout, cancellationToken);
}
// ── Secured Write Relay (M7 / T14b — approve → site MxGateway write) ──
// ── Secured Write Relay (approve → site MxGateway write) ──
/// <summary>
/// Relays a single tag write to the site's data connection and awaits the
@@ -538,7 +538,7 @@ public class CommunicationService
// ── Pattern 8: Heartbeat (site→central, Tell) ──
// Heartbeats are received by central, not sent. No method needed here.
// ── Inbound API Cross-Site Routing (WP-4) ──
// ── Inbound API Cross-Site Routing ──
/// <summary>
/// Routes an inbound API call to a site.
@@ -770,7 +770,7 @@ public class CommunicationService
}
/// <summary>
/// Task 5 (#22): relays an operator Retry of a parked cached call to its
/// Relays an operator Retry of a parked cached call to its
/// owning site. The <c>SiteCallAuditActor</c> is Asked directly (it is
/// central-local); it in turn relays a <c>RetryParkedOperation</c> to the
/// owning site and replies a <see cref="RetrySiteCallResponse"/> carrying a
@@ -796,7 +796,7 @@ public class CommunicationService
}
/// <summary>
/// Task 5 (#22): relays an operator Discard of a parked cached call to its
/// Relays an operator Discard of a parked cached call to its
/// owning site. See <see cref="RetrySiteCallAsync"/> for the routing and
/// source-of-truth rationale.
/// </summary>
@@ -6,7 +6,7 @@ using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;
namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <summary>
/// Canonical bridge for Audit Log (#23) rows between the in-process
/// Canonical bridge for Audit Log rows between the in-process
/// <see cref="AuditEvent"/> record and the wire-format <see cref="AuditEventDto"/>
/// exchanged over the <c>IngestAuditEvents</c>, <c>IngestCachedTelemetry</c> and
/// <c>PullAuditEvents</c> RPCs.
@@ -43,7 +43,7 @@ public static class AuditEventDtoMapper
{
ArgumentNullException.ThrowIfNull(evt);
// C3 (Task 2.5): the proto contract is the UNCHANGED 24-field wire. The
// The proto contract is the UNCHANGED 24-field wire. The
// canonical record carries the ScadaBridge domain fields inside
// DetailsJson — decompose them so the DTO's typed domain fields are
// populated exactly as before.
@@ -98,7 +98,7 @@ public static class AuditEventDtoMapper
{
ArgumentNullException.ThrowIfNull(dto);
// C3 (Task 2.5): recompose the canonical record from the 24-field wire
// Recompose the canonical record from the 24-field wire
// DTO. The domain fields are re-serialized into DetailsJson via the
// projection helper; IngestedAtUtc is left null (central sets it at
// ingest) and ForwardState is dropped (site-storage-only, never on the
@@ -5,7 +5,7 @@ using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;
namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <summary>
/// Canonical bridge for Site Call Audit (#22) operational rows between the
/// Canonical bridge for Site Call Audit operational rows between the
/// wire-format <see cref="SiteCallOperationalDto"/> exchanged on the
/// <c>CachedCallTelemetry</c> packet and the in-process <see cref="SiteCall"/>
/// persistence entity central writes into the <c>SiteCalls</c> table.
@@ -24,7 +24,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// 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>
/// onto the wire — used by the Site Call Audit <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
@@ -79,7 +79,7 @@ public static class SiteCallDtoMapper
/// <summary>
/// Projects a site-local <see cref="SiteCallOperational"/> onto its
/// wire-format DTO for the Site Call Audit (#22) <c>PullSiteCalls</c>
/// wire-format DTO for the Site Call Audit <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
@@ -42,7 +42,7 @@ public class SiteStreamGrpcClientFactory : IAsyncDisposable, IDisposable
/// already cached but bound to a *different* <paramref name="grpcEndpoint"/> — the
/// NodeA→NodeB failover flip, or a site whose gRPC address was edited — the stale
/// client is disposed and replaced with one bound to the requested endpoint.
/// Communication-012/013: keying purely by site identifier and ignoring the
/// Keying purely by site identifier and ignoring the
/// endpoint on a cache hit defeated debug-stream node failover and meant a
/// corrected gRPC address never took effect without a central restart.
/// </summary>
@@ -121,7 +121,7 @@ public class SiteStreamGrpcClientFactory : IAsyncDisposable, IDisposable
}
/// <summary>
/// Synchronous disposal. Communication-007: this used to block on
/// Synchronous disposal. This used to block on
/// <c>DisposeAsync().AsTask().GetAwaiter().GetResult()</c> (sync-over-async,
/// a stall/deadlock risk during host shutdown). Each
/// <see cref="SiteStreamGrpcClient"/> releases all of its resources
@@ -28,29 +28,29 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
private readonly int _maxConcurrentStreams;
private readonly TimeSpan _maxStreamLifetime;
private volatile bool _ready;
// Host-017 / REQ-HOST-7: flipped by CancelAllStreams() when the host enters
// Flipped by CancelAllStreams() when the host enters
// CoordinatedShutdown so SubscribeInstance refuses new streams with
// Unavailable before the actor system tears down. Strictly monotonic — once
// true, never reset (the server is single-lifetime per host).
private volatile bool _shuttingDown;
private long _actorCounter;
// Audit Log (#23 M2): central-side ingest actor proxy. Set by the host
// after the cluster singleton starts (see Bundle E wiring). When null the
// Central-side ingest actor proxy. Set by the host
// after the cluster singleton starts. When null the
// IngestAuditEvents RPC replies with an empty IngestAck so sites can
// safely retry — wiring-incomplete is treated as transient, never fatal.
private IActorRef? _auditIngestActor;
// Per Bundle D's brief — Ask timeout is 30 s. The ingest actor's repo
// Ask timeout is 30 s. The ingest actor's repo
// calls are sub-100 ms in steady state; a generous timeout absorbs a slow
// MSSQL connection without surfacing as a gRPC failure on a healthy site.
private static readonly TimeSpan AuditIngestAskTimeout = TimeSpan.FromSeconds(30);
// Audit Log (#23 M6): site-local queue handed in by AkkaHostedService on
// Audit Log: site-local queue handed in by AkkaHostedService on
// site roles so the central reconciliation puller's PullAuditEvents RPC
// can read Pending/Forwarded rows. Null when not wired (e.g. central-only
// host or test composing the server in isolation) — the handler treats
// 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
// Site Call Audit: 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
@@ -121,9 +121,9 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
/// <summary>
/// Hands the central-side <c>AuditLogIngestActor</c> proxy to the gRPC
/// server so the <see cref="IngestAuditEvents"/> RPC can route incoming
/// site batches. Audit Log (#23) M2 wiring point — mirrors the way
/// site batches. Mirrors the way
/// <c>CommunicationService.SetNotificationOutbox</c> takes the Notification
/// Outbox singleton proxy. Bundle E supplies the actor after the cluster
/// Outbox singleton proxy. The actor is supplied after the cluster
/// singleton starts.
/// </summary>
/// <param name="proxy">The audit log ingest actor proxy.</param>
@@ -135,7 +135,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
/// <summary>
/// Hands the site-local <see cref="ISiteAuditQueue"/> (the same
/// <c>SqliteAuditWriter</c> singleton that backs <see cref="IAuditWriter"/>
/// on the script thread) to the gRPC server so the M6
/// on the script thread) to the gRPC server so the
/// <see cref="PullAuditEvents"/> RPC can serve central's reconciliation
/// pulls. Mirrors <see cref="SetAuditIngestActor"/>: wired post-construction
/// because the queue and the gRPC server are both DI singletons brought up
@@ -151,7 +151,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
/// 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
/// the Site Call Audit <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.
@@ -163,7 +163,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
}
/// <summary>
/// Host-017 / REQ-HOST-7: signals the gRPC server to begin its part of the
/// Signals the gRPC server to begin its part of the
/// site shutdown sequence — refuse new <see cref="SubscribeInstance"/>
/// streams with <see cref="StatusCode.Unavailable"/> and cancel every
/// active stream so its <c>await foreach</c> observes
@@ -192,7 +192,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
}
/// <summary>
/// Host-017: exposed for test assertions on the shutdown state.
/// Exposed for test assertions on the shutdown state.
/// </summary>
internal bool IsShuttingDown => _shuttingDown;
@@ -216,12 +216,12 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
if (!_ready)
throw new RpcException(new GrpcStatus(StatusCode.Unavailable, "Server not ready"));
// Host-017 / REQ-HOST-7: refuse new subscriptions during shutdown so
// Refuse new subscriptions during shutdown so
// CoordinatedShutdown can quiesce without racing fresh streams.
if (_shuttingDown)
throw new RpcException(new GrpcStatus(StatusCode.Unavailable, "Server shutting down"));
// Communication-014: correlation_id arrives off the wire on a public gRPC
// correlation_id arrives off the wire on a public gRPC
// endpoint and is used (below) to compose an Akka actor name. Akka actor names
// have a restricted character set — a id containing '/', whitespace, or other
// disallowed characters would make ActorOf throw InvalidActorNameException,
@@ -261,7 +261,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase
Props.Create(typeof(Actors.StreamRelayActor), request.CorrelationId, channel.Writer),
$"stream-relay-{request.CorrelationId}-{actorSeq}");
// Communication-021: the previous code called _streamSubscriber.Subscribe
// The previous code called _streamSubscriber.Subscribe
// OUTSIDE the try block that owns relay-actor cleanup. If Subscribe threw
// (stale instance name, index lookup fault, site runtime shutting down),
// the freshly-created relay actor, the _activeStreams entry, the