namespace ZB.MOM.WW.ScadaBridge.Communication; /// /// Configuration options for central-site communication, including per-pattern /// timeouts and transport heartbeat settings. /// public class CommunicationOptions { /// /// Central control-plane gRPC endpoints (preferred first), e.g. /// ["http://scadabridge-central-a:8083", "http://scadabridge-central-b:8083"]. Dialled by /// with sticky failover/failback. Required on every site /// node — gRPC (CentralControlService) is the only site→central transport. /// /// /// Sites reach central by container/host name, NOT via Traefik (which is HTTP/1 only; gRPC is /// h2c on the central node's dedicated CentralGrpcPort). /// public List CentralGrpcEndpoints { get; set; } = new(); /// Timeout for deployment commands (typically longest due to apply logic). public TimeSpan DeploymentTimeout { get; set; } = TimeSpan.FromMinutes(2); /// Timeout for lifecycle commands (disable, enable, delete). public TimeSpan LifecycleTimeout { get; set; } = TimeSpan.FromSeconds(30); /// Timeout for artifact deployment commands. public TimeSpan ArtifactDeploymentTimeout { get; set; } = TimeSpan.FromMinutes(1); /// Timeout for remote query requests (event logs, parked messages). public TimeSpan QueryTimeout { get; set; } = TimeSpan.FromSeconds(30); /// Timeout for integration call routing. public TimeSpan IntegrationTimeout { get; set; } = TimeSpan.FromSeconds(30); /// Timeout for debug view subscribe/unsubscribe handshake. public TimeSpan DebugViewTimeout { get; set; } = TimeSpan.FromSeconds(10); /// Timeout for health report acknowledgement (fire-and-forget, but bounded). public TimeSpan HealthReportTimeout { get; set; } = TimeSpan.FromSeconds(10); /// /// Notification Outbox: timeout for forwarding a buffered notification to central /// and awaiting its NotificationSubmitAck. A timeout is treated as a /// transient failure — the Store-and-Forward engine keeps the message buffered /// and retries the forward at the fixed retry interval. /// public TimeSpan NotificationForwardTimeout { get; set; } = TimeSpan.FromSeconds(30); /// /// Audit Log: timeout for the SITE-side Ask that forwards one audit-telemetry batch /// (IngestAuditEventsCommand / IngestCachedTelemetryCommand) through the site's /// SiteCommunicationActor and awaits central's ack. Deliberately the LONGEST rung of the /// ingest timeout ladder. /// /// /// /// The ladder is strictly monotonic, outermost first: /// AuditForwardTimeout (35 s, this option) > /// SiteStreamGrpcServer.AuditIngestAskTimeout (30 s — both the gRPC call deadline and /// central's own Ask of the ingest singleton) > /// AuditLogIngestActor.IngestBudget (20 s) > /// AuditLogIngestActor.IngestSqlCommandTimeout (15 s). /// /// /// Strictness matters: it used to reuse (30 s), the /// same value as the rung below it, so a slow-but-succeeding central write could be acked to a /// caller whose Ask had already expired — the drain loop would treat the batch as unsent and /// re-ship it. Central dedups on EventId, so the duplicate was harmless but the retry /// traffic and the "stalled" telemetry signal were not. A timeout here is still transient: the /// rows stay Pending and drain on the next tick. /// /// /// enforces the outermost inequality — this value /// must be strictly greater than the 30 s gRPC/central Ask rung. /// /// public TimeSpan AuditForwardTimeout { get; set; } = TimeSpan.FromSeconds(35); /// /// Preshared key authenticating this node's gRPC control plane — the site↔central /// boundary. On a site node this is the key its inbound gate /// (ControlPlaneAuthInterceptor) expects on every SiteStreamService call, and /// which central must present; central resolves the matching value per site from its own /// secret store under the name SB-GRPC-PSK-{siteId}. /// /// /// /// In production this is supplied as ${secret:SB-GRPC-PSK-<siteId>} and expanded /// out of the secrets store before the host is built, so the plaintext never sits in /// appsettings. Development rigs set a literal, mirroring the LocalDb replication key. /// /// /// Empty means closed, not open. With no key set the interceptor rejects every gated /// call. This is not optional configuration: a node that ships without a key serves no /// streams and no audit pulls. /// /// /// Distinct from LocalDb:Replication:ApiKey, which authenticates the pair partner for /// database replication over the same listener. The two are never shared. /// /// public string GrpcPsk { get; set; } = ""; /// /// Central-side per-site gRPC preshared keys, keyed by site identifier — the mirror image /// of , which is the single key a site node expects on its own inbound /// gate. An entry here takes precedence over the secret store for that site. /// /// /// /// Why both a config map and a secret store. The store is the primary source and the /// only one that works for the real case: sites are added at runtime from the Central UI, so /// their keys cannot be enumerated in configuration at boot, and SitePskProvider /// resolves SB-GRPC-PSK-{siteId} on demand. This map covers the cases the store /// cannot or should not: a development rig that runs with no master key and injects every /// credential as an environment override, and an operator pinning one site's key without /// touching the store. Values may themselves be ${secret:…} references, since a map /// declared in configuration IS enumerable at boot. /// /// /// Absence is not a fallback to "unauthenticated" in either source — a site with no key in /// the map and none in the store cannot be dialed at all. /// /// public Dictionary SitePsks { get; set; } = new(); /// gRPC keepalive ping interval for streaming connections. public TimeSpan GrpcKeepAlivePingDelay { get; set; } = TimeSpan.FromSeconds(15); /// gRPC keepalive ping timeout — stream is considered dead if no response within this period. public TimeSpan GrpcKeepAlivePingTimeout { get; set; } = TimeSpan.FromSeconds(10); /// Maximum lifetime for a single gRPC stream before the server forces re-establishment. public TimeSpan GrpcMaxStreamLifetime { get; set; } = TimeSpan.FromHours(4); /// Maximum number of concurrent gRPC streaming subscriptions per site node. public int GrpcMaxConcurrentStreams { get; set; } = 100; /// /// Send-channel capacity for a per-instance Debug View stream (SubscribeInstance). /// Lossy by design: the Debug View is a diagnostic surface and drops its oldest events /// under backpressure rather than stalling the site's event hub. /// public int GrpcInstanceStreamChannelCapacity { get; set; } = 1000; /// /// Send-channel capacity for the site-wide alarm stream (SubscribeSite). Larger /// than the Debug View's (WP2.3): that feed backs the operator Alarm Summary, where a /// silently dropped transition is a missed alarm rather than a missed diagnostic frame, /// and an alarm burst arriving during a WAN stall must survive the stall. /// public int GrpcSiteAlarmStreamChannelCapacity { get; set; } = 20_000; /// Akka.Remote transport heartbeat interval. public TimeSpan TransportHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(5); /// /// Application-level site→central heartbeat cadence. Distinct from /// (the Akka.Remote failure-detector /// setting) — tuning the transport FD must not silently retune the health /// heartbeat. Default equals the old effective value (5s) — no behavior change. /// public TimeSpan ApplicationHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(5); /// Akka.Remote transport failure detection threshold. public TimeSpan TransportFailureThreshold { get; set; } = TimeSpan.FromSeconds(15); /// /// Base URL (Traefik/LB) the SITE uses to fetch deploy configs from central, /// e.g. "https://central.example:9000". Carried in RefreshDeploymentCommand so /// sites need no new standing config. Empty disables notify-and-fetch fallback. /// public string CentralFetchBaseUrl { get; set; } = ""; /// /// How long a staged PendingDeployment (and its fetch token) stays valid. Must /// comfortably cover both site nodes' fetches within one deploy window. /// public TimeSpan PendingDeploymentTtl { get; set; } = TimeSpan.FromMinutes(5); /// /// How often the central PendingDeploymentPurgeActor singleton reclaims /// expired (TTL-elapsed) PendingDeployment staging rows. Best-effort hygiene only: /// supersession bounds pending rows to ≤1 per instance and the config-fetch endpoint /// already enforces the TTL, so this purge merely sweeps rows left behind by instances /// that are deployed once and never re-deployed. Default 1 hour ≫ . /// public TimeSpan PendingDeploymentPurgeInterval { get; set; } = TimeSpan.FromHours(1); // ── Aggregated live alarm cache (plan #10, Task 4) ─────────────────────────── // Introduced by Task 4 with sane defaults; Task 6 formalizes eager validation // (positive interval/linger, positive concurrency/subscriber cap) and telemetry. /// /// How long the shared per-site live alarm aggregator keeps its gRPC stream + /// in-memory cache warm after its LAST Alarm Summary viewer leaves, before it is /// torn down. A short linger avoids re-seed thrash when an operator navigates away /// and straight back. Default 30s. /// public TimeSpan LiveAlarmCacheLinger { get; set; } = TimeSpan.FromSeconds(30); /// /// Cadence of the per-site aggregator's periodic reconcile snapshot fan-out. The /// backstop that corrects instance-set drift (enable/disable/deploy/delete) and any /// alarm delta missed by the live stream, since the aggregated stream never carries /// an explicit "removed" event. Default 60s. /// public TimeSpan LiveAlarmCacheReconcileInterval { get; set; } = TimeSpan.FromSeconds(60); /// /// Max concurrent per-instance snapshot fetches during a live-cache seed/reconcile /// fan-out — the aggregated analogue of the Alarm Summary poll's /// MaxConcurrentFetches. Default 8. /// public int LiveAlarmCacheSeedConcurrency { get; set; } = 8; /// /// Upper bound on concurrent Alarm Summary viewers sharing one site's aggregator. /// A defensive cap only — one gRPC stream per site is shared regardless of viewer /// count; this just bounds the subscriber list. Default 200. /// public int LiveAlarmCacheMaxSubscribersPerSite { get; set; } = 200; /// /// Publish-coalescing window for live alarm deltas: an applied delta marks the cache /// dirty and one publish (fresh snapshot + per-viewer onChanged fan-out) fires after /// this window, batching an alarm storm into ~4 publishes/second instead of one per /// transition (review 02 round 2, N6). Zero = publish per delta (legacy). Seed and /// reconcile publishes are always immediate. Default 250 ms. /// public TimeSpan LiveAlarmCachePublishCoalesce { get; set; } = TimeSpan.FromMilliseconds(250); /// /// Random jitter added to each per-site reconcile tick, as a fraction of /// (WP2.3). Without it every aggregator /// started by one central failover fans its whole-site snapshot out on the same 60s /// boundary forever. Zero disables jitter. /// public double LiveAlarmCacheReconcileJitterFraction { get; set; } = 0.2; }