refactor(comm): retire ClusterClient naming after the gRPC cutover

Phase 4 of the ClusterClient -> gRPC migration deleted the Akka transports
but left the naming behind: `ClusterClientSiteAuditClient` was transport-
agnostic and worked unchanged, so it survived the deletion under a name
that now describes a transport the repo no longer has. Same for a scatter
of doc-comments still framing gRPC as "the new transport" beside an Akka
one that is gone.

Renames it to `SiteCommunicationAuditClient` (and its test file) and
rewrites the stale comments to describe the single transport that exists.
Also tightens CLAUDE.md: drops the self-describing directory listing and
the 27-component enumeration in favour of the non-obvious parts only.

Behaviour-neutral: names and prose only. Recorded as the Phase 4
follow-up in docs/plans/2026-07-22-clusterclient-to-grpc-plan.md.
This commit is contained in:
Joseph Doherty
2026-07-27 15:40:01 -04:00
parent b3dc17a5fe
commit 63c16d6912
57 changed files with 193 additions and 244 deletions
@@ -89,7 +89,7 @@ public class CentralCommunicationActor : ReceiveActor
/// Default Ask timeout for routing audit ingest commands to the
/// Effective Ask timeout for audit ingest routing. Defaults to
/// <see cref="Grpc.SiteStreamGrpcServer.AuditIngestAskTimeout"/> (30 s) — the two
/// audit-ingest transports (gRPC vs ClusterClient) now share one source of truth
/// audit-ingest entry points (the site stream server and the control plane) share one source of truth
/// for the timeout. Overridable via the constructor so tests can exercise the
/// timeout/fault path without waiting 30 s. When the window is exceeded the Ask
/// faults and that fault is piped back to the caller as a
@@ -167,8 +167,8 @@ public class CentralCommunicationActor : ReceiveActor
});
// Notification Outbox ingest: a site forwards a buffered NotificationSubmit to the
// central cluster via ClusterClient. Forward to the outbox proxy so the original
// Sender (the site's ClusterClient path) is preserved and the NotificationSubmitAck
// central cluster. Forward to the outbox proxy so the original
// Sender (the site's transport path) is preserved and the NotificationSubmitAck
// routes straight back to the site.
Receive<NotificationSubmit>(HandleNotificationSubmit);
@@ -186,9 +186,9 @@ public class CentralCommunicationActor : ReceiveActor
});
// Audit Log site→central ingest: a site forwards a batch of audit
// events to the central cluster via ClusterClient. Ask the ingest proxy
// events to the central cluster. 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.
// site's transport path) so the site can flip its rows to Forwarded.
Receive<IngestAuditEventsCommand>(HandleIngestAuditEvents);
// Audit Log combined-telemetry ingest: routes to the same proxy
@@ -196,9 +196,9 @@ public class CentralCommunicationActor : ReceiveActor
Receive<IngestCachedTelemetryCommand>(HandleIngestCachedTelemetry);
// Startup reconciliation: a site node forwards its local deployed inventory on
// startup via ClusterClient. Resolve the scoped ReconcileService, diff the
// startup. Resolve the scoped ReconcileService, diff the
// inventory against central's expected set, and pipe the ReconcileSiteResponse
// (gap fetch tokens + orphans) straight back to the site node's ClusterClient.
// (gap fetch tokens + orphans) straight back to the site node.
Receive<ReconcileSiteRequest>(HandleReconcileSiteRequest);
}
@@ -254,7 +254,7 @@ public class CentralCommunicationActor : ReceiveActor
}
// Capture Sender before the async/PipeTo — Akka resets Sender between
// dispatches. The reply is piped straight back to the site's ClusterClient.
// dispatches. The reply is piped straight back to the calling site node.
// On an Ask timeout or a faulted reply, PipeTo delivers a Status.Failure to
// replyTo: the fault propagates to the caller rather than being swallowed.
// The site's own Ask through this path then faults, and the site drain loop
@@ -285,10 +285,10 @@ public class CentralCommunicationActor : ReceiveActor
}
/// <summary>
/// Startup reconciliation (site→central over ClusterClient): resolve the scoped
/// Startup reconciliation (site→central): resolve the scoped
/// <see cref="ReconcileService"/> in a DI scope, diff the node's reported inventory
/// against central's expected set, and pipe the <see cref="ReconcileSiteResponse"/>
/// back to the site node's ClusterClient path. The actor stays thin — all the diff
/// back to the site node's transport path. The actor stays thin — all the diff
/// and staging logic lives in the service. Mirrors the DB-access pattern used by
/// <see cref="LoadSiteAddressesFromDb"/> (Task.Run + CreateScope + PipeTo) and the
/// Sender-preservation pattern of <see cref="HandleIngestAuditEvents"/>.
@@ -331,7 +331,7 @@ public class CentralCommunicationActor : ReceiveActor
MarkHeartbeatLocally(heartbeat);
// Fan the heartbeat out to the peer central node so BOTH aggregators mark
// it, regardless of which central node the site's ClusterClient delivered
// it, regardless of which central node the site delivered
// to. Without this, a heartbeat that only ever reaches one node leaves the
// other node's aggregator blind to that site's liveness after a failover
// (arch review 02, Low). MarkHeartbeat is idempotent (timestamp overwrite),
@@ -358,7 +358,7 @@ public class CentralCommunicationActor : ReceiveActor
}
/// <summary>
/// Handles a report delivered directly from a site (via ClusterClient):
/// Handles a report delivered directly from a site:
/// process locally, then fan out to the peer central node so its
/// aggregator stays in sync.
/// </summary>
@@ -407,7 +407,7 @@ public class CentralCommunicationActor : ReceiveActor
// 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).
// Ask timeout).
private void HandleSiteEnvelope(SiteEnvelope envelope)
{
@@ -491,8 +491,8 @@ public class CentralCommunicationActor : ReceiveActor
private void HandleSiteAddressCacheLoaded(SiteAddressCacheLoaded msg)
{
// Per-transport per-site resource reconciliation (create/stop ClusterClients, or
// build/drop gRPC channel pairs). Runs on the actor thread each refresh tick.
// Per-site transport resource reconciliation (build/drop gRPC channel
// pairs). Runs on the actor thread each refresh tick.
_transport.ReconcileSites(msg);
// Self-healing eviction: a site deleted from configuration would otherwise
@@ -525,7 +525,7 @@ public class CentralCommunicationActor : ReceiveActor
// Subscribe to the peer-replication topic so we receive health reports
// delivered to the other central node and keep our local aggregator
// in sync (ClusterClient load-balances reports across nodes).
// in sync (a site may deliver its report to either central node).
// Tolerant of non-clustered hosts (TestKit) where the extension is absent.
try
{
@@ -581,7 +581,7 @@ public record RefreshSiteAddresses;
/// discipline. The producer wraps the constructed buckets with
/// <c>List&lt;T&gt;.AsReadOnly()</c> before piping to Self.
/// </summary>
/// <param name="SiteContacts">Akka ClusterClient contact addresses per site (from NodeA/NodeBAddress).</param>
/// <param name="SiteContacts">Akka remote addresses per site (from NodeA/NodeBAddress).</param>
/// <param name="KnownSiteIds">Every configured site id, address-bearing or not, for aggregator pruning.</param>
/// <param name="GrpcContacts">
/// gRPC endpoint pairs per site (from GrpcNodeA/GrpcNodeBAddress) — the streaming path's columns,
@@ -603,8 +603,8 @@ public readonly record struct SiteGrpcEndpoints(string? NodeA, string? NodeB);
/// <summary>
/// Peer-replication envelope for a site heartbeat, fanned out over the same
/// DistributedPubSub topic as <see cref="SiteHealthReportReplica"/> so both
/// central aggregators mark heartbeats regardless of which node the site's
/// ClusterClient delivered to. Only ever travels central↔central (same assembly
/// central aggregators mark heartbeats regardless of which node the site
/// delivered to. Only ever travels central↔central (same assembly
/// version, rolled together), so the default reflective serializer is safe.
/// </summary>
public sealed record SiteHeartbeatReplica(HeartbeatMessage Heartbeat);
@@ -94,10 +94,10 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
/// <summary>
/// Initializes the debug stream bridge actor and registers message handlers.
/// </summary>
/// <param name="siteIdentifier">Site identifier for targeting ClusterClient messages and logging.</param>
/// <param name="siteIdentifier">Site identifier for targeting site-addressed messages and logging.</param>
/// <param name="instanceUniqueName">Unique name of the instance whose debug stream is being bridged.</param>
/// <param name="correlationId">Correlation id for the debug session.</param>
/// <param name="centralCommunicationActor">Actor used to forward ClusterClient messages to the site.</param>
/// <param name="centralCommunicationActor">Actor used to forward site-addressed messages to the site.</param>
/// <param name="onEvent">Callback invoked on each received debug event.</param>
/// <param name="onTerminated">Callback invoked when the stream terminates.</param>
/// <param name="grpcFactory">Factory for creating gRPC streaming clients.</param>
@@ -124,7 +124,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers
_grpcNodeAAddress = grpcNodeAAddress;
_grpcNodeBAddress = grpcNodeBAddress;
// Initial snapshot response from the site (via ClusterClient).
// Initial snapshot response from the site.
// 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
@@ -4,13 +4,11 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Actors;
/// <summary>
/// The central→site command-send seam, injected into <see cref="CentralCommunicationActor"/>
/// below the <see cref="SiteEnvelope"/> handler. Exactly one implementation is active per node,
/// chosen by <c>ScadaBridge:Communication:SiteTransport</c>:
/// <list type="bullet">
/// <item><see cref="AkkaSiteTransport"/> — today's per-site <c>ClusterClient</c> path (default).</item>
/// <item><see cref="ZB.MOM.WW.ScadaBridge.Communication.Grpc.GrpcSiteTransport"/> — the site
/// <c>SiteCommandService</c> gRPC plane.</item>
/// </list>
/// below the <see cref="SiteEnvelope"/> handler. The sole implementation is
/// <see cref="ZB.MOM.WW.ScadaBridge.Communication.Grpc.GrpcSiteTransport"/>, over the site
/// <c>SiteCommandService</c> gRPC plane. The seam predates that: it was introduced so the Akka
/// <c>ClusterClient</c> path and the gRPC plane could run side by side during the migration, and
/// is kept because it is the natural substitution point for tests.
/// The producers above the seam (<c>CommunicationService</c>'s 27 commands, <c>SiteCallAuditActor</c>'s
/// 2 parked relays, and <c>DebugStreamBridgeActor</c>'s subscribe/unsubscribe) are unchanged — they
/// still <c>Ask</c>/<c>Tell</c> a <see cref="SiteEnvelope"/> to the actor, which delegates here.
@@ -27,14 +25,14 @@ public interface ISiteCommandTransport
/// delivered to <paramref name="replyTo"/> — for an <c>Ask</c> that is the temporary ask actor
/// (completing the caller's task); for a <c>Tell</c>-with-sender (the debug bridge) that is the
/// originating actor. A message with no route (an unknown site) is warned and dropped so the
/// caller's <c>Ask</c> times out, exactly as today's ClusterClient path behaves.
/// caller's <c>Ask</c> times out — central never buffers for an unreachable site.
/// </summary>
/// <param name="envelope">The site-addressed command envelope.</param>
/// <param name="replyTo">Where a reply (or a <see cref="Status.Failure"/>) is delivered.</param>
void Send(SiteEnvelope envelope, IActorRef replyTo);
/// <summary>
/// Reconciles per-site transport resources (ClusterClients for Akka, channel pairs for gRPC)
/// Reconciles per-site transport resources (gRPC channel pairs)
/// against the freshly loaded site set. Called once per DB refresh tick with the same cache
/// message the actor already receives — the ONE DB-poll loop feeds both transports.
/// </summary>
@@ -536,7 +536,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers
if (_retryCount > MaxRetries)
{
// Give up the stream, but do NOT stop the aggregator: the periodic reconcile
// still refreshes the cache from ClusterClient snapshots, so the page keeps a
// still refreshes the cache from site snapshots, so the page keeps a
// (slower) live-ish view rather than going dark. A later reconcile-triggered
// reconnect is not attempted here; the stream is simply left down.
_log.Error("Site-alarm gRPC stream for {0} exceeded max retries ({1}); leaving stream down, " +
@@ -21,8 +21,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Actors;
/// </summary>
/// <remarks>
/// <para>
/// Two transports call this one unit: the Akka <see cref="SiteCommunicationActor"/>
/// (ClusterClient) and the new <c>SiteCommandGrpcService</c> (gRPC). Centralising the
/// Two entry points call this one unit: the site-side <see cref="SiteCommunicationActor"/>
/// and <c>SiteCommandGrpcService</c>. Centralising the
/// table means the two can never drift on where a command goes. Each transport keeps
/// its own send mechanics — the actor <c>Forward</c>s (preserving the Ask sender), the
/// gRPC service <c>Ask</c>s and encodes the reply — but both read the same
@@ -219,9 +219,9 @@ public sealed class SiteCommandDispatcher
private static Route Immediate(object reply) => new(RouteDisposition.ImmediateReply, null, reply);
/// <summary>
/// The actor's failover path: resolve the standby AND issue the leave in one step (today's
/// coupled behaviour over ClusterClient, where <c>Tell</c> merely enqueues the ack so leave
/// order is immaterial), then return the ack.
/// The actor's failover path: resolve the standby AND issue the leave in one step (coupled,
/// because <c>Tell</c> merely enqueues the ack so leave order is immaterial here), then
/// return the ack.
/// </summary>
/// <param name="msg">The failover command.</param>
/// <returns>The ack to send back.</returns>
@@ -283,8 +283,8 @@ public sealed class SiteCommandDispatcher
}
catch (Exception ex)
{
// A fault must be reported to the operator, never thrown into supervision (over
// ClusterClient) or surfaced as a broken stream (over gRPC).
// A fault must be reported to the operator, never thrown into supervision or
// surfaced as a broken stream (over gRPC).
return new FailoverOutcome(
new SiteFailoverAck(
msg.CorrelationId, Accepted: false, TargetAddress: null, ErrorMessage: ex.Message),
@@ -16,9 +16,10 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
namespace ZB.MOM.WW.ScadaBridge.Communication.Actors;
/// <summary>
/// Site-side actor that receives messages from central via ClusterClient and routes
/// them to the appropriate local actors. Also sends heartbeats and health reports
/// to central via the registered ClusterClient.
/// Site-side actor that receives messages from central over the site-hosted
/// <c>SiteCommandService</c> gRPC plane and routes them to the appropriate local actors.
/// Also sends heartbeats and health reports to central over its
/// <see cref="ICentralTransport"/> (<see cref="Grpc.GrpcCentralTransport"/> in production).
///
/// Routes all 8 message patterns to local handlers.
/// </summary>
@@ -157,15 +158,15 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
// the SITE-SPECIFIC role, because that is what site singletons (the Deployment
// Manager) are placed on. Either node may receive this (the actor is per-node, not a
// singleton, and contact rotation picks whichever answers); the target is resolved
// from cluster state, not from who received the message. Over ClusterClient the ack
// Tell merely enqueues, so the dispatcher resolves-and-leaves in one step (the gRPC
// transport defers the leave to keep ack-before-Leave — see PrepareFailover).
// from cluster state, not from who received the message. The gRPC transport defers the
// leave to keep ack-before-Leave — see PrepareFailover. (Under the removed ClusterClient
// path the ack Tell merely enqueued, so the dispatcher resolved-and-left in one step.)
Receive<TriggerSiteFailover>(msg => Sender.Tell(_dispatcher.HandleFailover(msg)));
// The seven site→central sends now delegate to the injected transport (ClusterClient by
// default, gRPC when configured). Each handler captures the current Sender as the reply
// target so central's reply routes straight back to the waiting Ask, not through this
// actor — the exact sender-forwarding the ClusterClient path relied on. The per-message
// The seven site→central sends delegate to the injected transport (GrpcCentralTransport in
// production). Each handler captures the current Sender as the reply target so central's
// reply routes straight back to the waiting Ask, not through this actor — preserving the
// sender-forwarding the original ClusterClient path relied on. The per-message
// "no transport / not-accepted" fallbacks live inside the transport now.
// Notification Outbox: forward a buffered notification (S&F forwarder's Ask → ack back).
@@ -229,9 +230,9 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers
/// Executes a resolved command route within the actor: Forward to the target (preserving the
/// central Ask sender so the reply routes straight back to the waiting Ask), or — when a
/// null-guarded handler is unregistered — Tell the caller the dispatcher's synthetic reply.
/// The fire-and-forget disposition (UnsubscribeDebugView) is a plain Forward here, exactly as
/// before: over ClusterClient the site never acked it, so the synthetic ack is a gRPC-only
/// concern.
/// The fire-and-forget disposition (UnsubscribeDebugView) is a plain Forward here: the site
/// never acked it under the original ClusterClient path either, so the synthetic ack is a
/// gRPC-only concern.
/// </summary>
/// <param name="command">The migrated central→site command to route.</param>
private void DispatchCommand(object command)
@@ -13,7 +13,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc {
///
/// Direction: SITE is the client, CENTRAL is the server — the inverse of
/// SiteStreamService, where central dials the site. That asymmetry is deliberate
/// and mirrors the direction the Akka ClusterClient traffic flows today: these
/// and mirrors the direction the Akka ClusterClient traffic used to flow: these
/// seven calls are exactly the seven messages SiteCommunicationActor sends to
/// /user/central-communication.
///
@@ -139,7 +139,7 @@ public class CommunicationService
/// 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
/// gRPC command/control transport; the Ask times out (no
/// central buffering) if the site is unreachable, and the caller falls
/// through to a normal deploy.
/// </summary>
@@ -16,8 +16,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <para>
/// The seven pairs mirror, one for one, the seven messages
/// <c>SiteCommunicationActor</c> forwards to <c>/user/central-communication</c> over
/// Akka <c>ClusterClient</c> today. Both transports carry the SAME message types
/// end-to-end — central's handlers are untouched by the migration — so this mapper is
/// the control plane. The in-process and wire representations carry the SAME message types
/// end-to-end — central's handlers were untouched by the migration — so this mapper is
/// the only place the two representations meet, and a field that does not survive a
/// round-trip here is a field the gRPC transport silently drops.
/// </para>
@@ -14,7 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <summary>
/// Central-hosted gRPC face of the seven site→central control messages
/// (<c>Protos/central_control.proto</c>). Decodes each request onto the SAME in-process
/// message type the Akka <c>ClusterClient</c> path already carries, <c>Ask</c>s
/// message type the removed Akka <c>ClusterClient</c> path used to carry, <c>Ask</c>s
/// <see cref="Actors.CentralCommunicationActor"/>, and encodes the reply back.
/// </summary>
/// <remarks>
@@ -26,8 +26,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// </para>
/// <para>
/// <b>Zero handler logic lives here.</b> Every RPC lands on a receive
/// <c>CentralCommunicationActor</c> already implements for the ClusterClient path, so the two
/// transports cannot drift in behaviour: the actor is the single implementation, and this class
/// <c>CentralCommunicationActor</c> already implements, so transport and handler cannot drift
/// in behaviour: the actor is the single implementation, and this class
/// is a codec plus an <c>Ask</c>. That is also why the service takes the actor through
/// <see cref="SetReady"/> rather than resolving anything from DI — the actor is created by the
/// host's Akka bootstrap, not by the container.
@@ -36,8 +36,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <b>Fault semantics deliberately differ from <see cref="SiteStreamGrpcServer"/>'s ingest
/// RPCs.</b> That server answers a failed audit ingest with an EMPTY <c>IngestAck</c>; this one
/// fails the call with a non-OK status. Both leave the site's rows <c>Pending</c> for the next
/// drain, so the outcome is the same — but this service replaces the ClusterClient path, whose
/// documented behaviour is to propagate the fault (<c>CentralCommunicationActor</c>'s
/// drain, so the outcome is the same — but this service replaced the ClusterClient path, whose
/// documented behaviour was to propagate the fault (<c>CentralCommunicationActor</c>'s
/// <c>HandleIngestAuditEvents</c> pipes a <c>Status.Failure</c> back), and preserving that keeps
/// a lost batch visible as a failure rather than as a successful call that acked nothing.
/// </para>
@@ -94,8 +94,8 @@ public sealed class CentralControlGrpcService : CentralControlService.CentralCon
/// actor exists and can receive, NOT that every downstream singleton proxy
/// (<c>notification-outbox</c>, <c>audit-log-ingest</c>) has registered itself yet. Those
/// register moments later in the same startup path, and the actor already answers a call
/// that beats them with the same "not available, retry" reply it gives on the ClusterClient
/// path — so gating readiness on them would add nothing but a longer window in which sites
/// that beats them with a "not available, retry" reply
/// — so gating readiness on them would add nothing but a longer window in which sites
/// see <see cref="StatusCode.Unavailable"/>.
/// </remarks>
/// <param name="centralCommunicationActor">The central communication actor.</param>
@@ -11,8 +11,8 @@ using AkkaStatus = Akka.Actor.Status;
namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <summary>
/// The <see cref="ICentralTransport"/> that carries the seven site→central sends over gRPC — the
/// migration target for the Akka <c>ClusterClient</c> path. Each method encodes the message with
/// The <see cref="ICentralTransport"/> that carries the seven site→central sends over gRPC —
/// the replacement for the removed Akka <c>ClusterClient</c> path. Each method encodes the message with
/// <see cref="CentralControlDtoMapper"/>, dials <c>CentralControlService</c> through the sticky
/// <see cref="CentralChannelProvider"/>, and delivers the decoded reply (or a transient-failure
/// signal) to the waiting Ask.
@@ -81,7 +81,7 @@ public sealed class GrpcSiteTransport : ISiteCommandTransport
}
catch (SiteChannelUnavailableException)
{
// Parity with the Akka "no ClusterClient for site" path: warn and drop, so the caller's
// Parity with the removed Akka "no ClusterClient for site" path: warn and drop, so the caller's
// Ask times out. Central never buffers.
_logger.LogWarning(
"No gRPC channel for site {SiteId}; dropping {Message} (caller's Ask will time out)",
@@ -22,7 +22,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// </para>
/// <para>
/// <b>Coexistence — server-side only.</b> This makes the site ALSO listen on gRPC for commands;
/// nothing central flips to gRPC here (that is T1B.3). Central still dials sites via ClusterClient.
/// this was the server half, landed before central was flipped to dial it (T1B.3).
/// </para>
/// <para>
/// <b>Fire-and-forget.</b> The Akka path never acks <c>UnsubscribeDebugView</c>, but a unary RPC
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;
/// <summary>
/// Raised when a site has no usable gRPC channel — an unknown site, or a site with neither
/// <c>GrpcNodeAAddress</c> nor <c>GrpcNodeBAddress</c> configured. The gRPC transport treats this
/// the way the Akka path treats "no ClusterClient for site": warn and drop, so the caller's Ask
/// the way the removed Akka path treated "no ClusterClient for site": warn and drop, so the caller's Ask
/// times out (central never buffers).
/// </summary>
public sealed class SiteChannelUnavailableException(string siteId)
@@ -17,7 +17,7 @@ import "Protos/sitestream.proto";
//
// Direction: SITE is the client, CENTRAL is the server — the inverse of
// SiteStreamService, where central dials the site. That asymmetry is deliberate
// and mirrors the direction the Akka ClusterClient traffic flows today: these
// and mirrors the direction the Akka ClusterClient traffic used to flow: these
// seven calls are exactly the seven messages SiteCommunicationActor sends to
// /user/central-communication.
//
@@ -8,8 +8,8 @@ namespace ZB.MOM.WW.ScadaBridge.Communication;
/// <summary>
/// Central-side startup-reconciliation handler. A site node, on startup, reports its
/// local deployed inventory via <see cref="ReconcileSiteRequest"/> (delivered over
/// ClusterClient to <see cref="Actors.CentralCommunicationActor"/>); this service diffs
/// local deployed inventory via <see cref="ReconcileSiteRequest"/> (delivered over the
/// control plane to <see cref="Actors.CentralCommunicationActor"/>); this service diffs
/// it against central's expected deployed set and replies with fresh fetch tokens for the
/// gap — instances the node is missing or has at a stale revision — plus the orphan names
/// (present locally but no longer deployed centrally, which the node only logs).