namespace ZB.MOM.WW.ScadaBridge.SiteRuntime; /// /// Configuration options for the Site Runtime component. /// Bound from ScadaBridge:SiteRuntime configuration section. /// public class SiteRuntimeOptions { /// /// Number of Instance Actors to create per batch during staggered startup. /// Default: 20. /// public int StartupBatchSize { get; set; } = 20; /// /// Delay in milliseconds between startup batches to prevent reconnection storms. /// Default: 100ms. /// public int StartupBatchDelayMs { get; set; } = 100; /// /// Maximum call depth for recursive script calls (CallScript/CallShared). /// Default: 10. /// public int MaxScriptCallDepth { get; set; } = 10; /// /// Default script execution timeout in seconds. /// Default: 30 seconds. /// public int ScriptExecutionTimeoutSeconds { get; set; } = 30; /// /// Per-subscriber buffer size for the site-wide Akka stream. /// Slow subscribers drop oldest messages when buffer is full. /// Default: 1000. /// public int StreamBufferSize { get; set; } = 1000; /// /// FLOOR for the number of dedicated threads in the script-execution scheduler. /// Script and alarm on-trigger bodies run on these threads instead of the shared /// .NET thread pool, so blocking script I/O cannot starve the global pool. /// /// WP3.1: this was a fixed size and is now the lower bound of a grow-only, /// instance-scaled pool — see /// . Existing /// configurations keep exactly the previous behaviour at or below /// ScriptExecutionThreadCount * InstancesPerScriptThread deployed instances. /// /// Default: 8. /// public int ScriptExecutionThreadCount { get; set; } = 8; /// /// WP3.1: CEILING for the instance-scaled script-execution pool. The pool grows /// towards ceil(enabledInstances / 8) threads but never past this value; beyond /// it, is the real regulator. A 1 MB-stack /// dedicated thread is cheap, so 32 (≈ 256 instances at the /8 ratio) is a generous /// default. Must be greater than or equal to . /// Default: 32. /// public int ScriptExecutionMaxThreadCount { get; set; } = 32; /// /// WP3.1 (finding #4): maximum number of trigger-expression evaluations allowed to run /// concurrently across the whole process. Trigger expressions are non-blocking by /// construction ( exposes only reads over an /// in-memory snapshot, and the script trust gate has already denied I/O, network, and /// threading), so they run as plain async work on the shared .NET thread pool behind this /// gate rather than on the blocking script-execution pool. That is what keeps an alarm's /// Expression trigger from queueing behind blocking script bodies. /// Default: max(2, Environment.ProcessorCount). /// public int TriggerEvalMaxConcurrency { get; set; } = Math.Max(2, Environment.ProcessorCount); /// /// WP3.1: timeout (seconds) for a single trigger-expression evaluation. Previously /// hardcoded at 2 s in both ScriptActor and AlarmActor. The deadline is now /// armed when the evaluation is ENQUEUED, not when it is dequeued, so time spent waiting /// on burns the same budget — a saturated gate /// produces a timely "false" instead of an unbounded stall. /// Default: 2. /// public int TriggerEvalTimeoutSeconds { get; set; } = 2; /// /// WP3.1: maximum number of concurrent runs (queued or executing) for any single script /// or alarm on-trigger script. A trigger that arrives while the cap is reached is SHED — /// the newest run is refused, the four already in flight (which are closest to their own /// deadlines) are kept, and no reordering occurs. A shed increments /// ISiteHealthCollector.IncrementScriptRunShed, emits a rate-limited Warning site /// event, and — for an Ask-based CallScript — replies with an explicit error rather /// than letting the caller hang to its Ask timeout. /// Default: 4. /// public int MaxConcurrentRunsPerScript { get; set; } = 4; /// /// Max mirrored native alarms retained per source binding before older entries are dropped (logged). /// Default: 1000. /// public int MirroredAlarmCapPerSource { get; set; } = 1000; /// /// Interval in milliseconds to retry a failed native alarm subscription. /// Default: 5000ms. /// public int NativeAlarmRetryIntervalMs { get; set; } = 5000; /// HTTP timeout (seconds) for fetching a deployment config from central (notify-and-fetch). public int ConfigFetchTimeoutSeconds { get; set; } = 30; /// /// Floor interval (ms) at which an Instance Actor re-sends a tag-subscribe request /// that either failed or whose response was lost (S4/UA6). The retry is armed on /// every send and cancelled on the first Success reply. As of Lever 1 (2026-08-28, /// MxGateway subscribe-storm prevention) it is the FLOOR of an exponential backoff: /// each failed/lost attempt doubles the interval (±20 % jitter) up to /// , and a Success or a connection /// returning to Good quality resets it here. Default: 5000ms. /// public int TagSubscribeRetryIntervalMs { get; set; } = 5000; /// /// Ceiling (ms) of the tag-subscribe retry backoff. Must be >= /// ; equal makes the interval fixed /// (pre-Lever-1 behaviour). Default: 60000ms. /// public int TagSubscribeRetryMaxIntervalMs { get; set; } = 60000; /// /// How long (ms) an outstanding tag-subscribe request blocks a new one for the /// same connection. While a request is in flight and younger than this, a retry /// tick only re-arms the watchdog instead of issuing a duplicate subscribe — this /// is what stops a slow gateway from receiving a fresh N-tag subscribe every /// interval on top of the one it is still processing. Once older than this the /// reply is presumed lost and a new request is allowed. Size it above the /// gateway's per-command deadline (MxGateway default 30 s). Default: 45000ms. /// /// Two consequences worth knowing: (1) the guard binds only while the retry interval /// is shorter than this ceiling — once the backoff has grown past it every tick is /// already older than the ceiling, so the interval itself becomes the rate limit /// (steady state at the defaults: one re-send per 60 s); (2) a connection returning to /// Good resets the backoff but does NOT release an outstanding request — that request /// still owns the connection until the ceiling, so the Good reset shortens the wait /// only when nothing is in flight. /// /// public int TagSubscribeInFlightCeilingMs { get; set; } = 45000; /// /// Grace period (ms) after a script's execution timeout elapses before the /// stuck-script watchdog declares the script's dedicated thread blocked and /// emits a loud site event naming it (S2). The CTS timeout only requests /// cooperative cancellation; a script blocked in synchronous I/O never /// observes it, so this watchdog is the only operator signal that one of the /// bounded script-execution threads is gone. Default: 30000ms. /// public int StuckScriptGraceMs { get; set; } = 30000; /// /// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer): /// capacity of the bounded hand-off queue feeding 's /// dedicated alarm publish path. Alarm state changes are staged through this /// queue into their OWN broadcast source — separate from the (far higher-volume) /// attribute publish source — so an attribute storm cannot evict a pending alarm /// transition. DropOldest overflow; drops are counted (surfaced on the site health /// report) and logged. Default: 2000 (alarm volume is inherently far lower than /// attribute volume, so this rarely needs to be anywhere near /// ). /// public int AlarmPublishQueueCapacity { get; set; } = 2000; /// /// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md): /// global switch restoring the legacy per-run Started/Completed Info site events for /// EVERY instance script run. Off by default: those two rows per run were the dominant /// site_events writer (~12 physical row-writes across a replicated pair per run, /// counting CDC oplog + row-version rows and their eventual purge tombstones), for /// content nobody looks at unless something went wrong. With this off, an interval /// aggregate () replaces the per-run rows /// and script Error events (timeout/failure/stuck-watchdog) are unaffected — they are /// unconditional regardless of this switch. Read via /// at the top of /// every run, so it is hot-togglable (no restart, no redeploy). Default: false. /// public bool PerRunScriptEvents { get; set; } /// /// WP3.2: per-script opt-in restoring the legacy per-run Started/Completed Info site /// events for NAMED scripts only, for live debugging without flipping the global /// switch (and therefore without paying its volume /// cost for every other script). Effective only while the global switch is off. /// Entries are "{InstanceName}/{ScriptName}" for an exact match, or /// "{InstanceName}/*" to opt in every script on that instance. Matched by /// , read from /// per run — hot /// -togglable like . Default: empty (no opt-ins). /// public List PerRunScriptEventScripts { get; set; } = new(); /// /// WP3.2: flush interval (seconds) for 's /// interval aggregate — one "script" Info site event per interval summarizing every /// instance script run since the last flush (headline totals + a top-50-scripts /// breakdown, capped so a pathological deployment cannot mint an oversized row). An /// interval with zero activity emits no row. 0 disables the summary flush /// entirely (the recorder still accumulates counters; they are just never emitted or /// reset). Default: 300 (5 minutes) — at most 288 summary rows/day, versus 2 rows per /// script run under the legacy per-run behaviour. /// public int ScriptRunSummaryIntervalSeconds { get; set; } = 300; /// /// Interval (seconds) of the Deployment Manager singleton's instance reconcile sweep /// (deferred register rows 78 + 72). Every tick the singleton re-reads the durable /// deployed_configurations rows and repairs two silent divergences between the /// stored configs and the running Instance Actors: /// /// an ENABLED instance with a stored config and NO running actor is started /// (row 78 — configs that arrive by LocalDb CDC replication are written straight into the /// file by the sync applier, so they are invisible to both the boot-time load, which has /// already run, and the startup reconcile, which reports no gap because central's view is /// correct); /// a running instance whose stored revision hash no longer matches the revision it /// was started from is re-applied through the ordinary redeploy path (row 72 — the startup /// reconcile refreshes a stale config row but leaves the running actor serving the config /// central believes it replaced). /// /// The sweep is silent when there is nothing to repair. 0 disables it entirely /// (the row-67 EnsureInstanceStartedCommand path is unaffected). Default: 60. /// public int InstanceSweepIntervalSeconds { get; set; } = 60; /// /// Flow Debugger trace arm window (design 2026-09-05 §5.3). When a TRACED script write /// targets a data-sourced attribute, the Instance Actor remembers /// (attribute → writer's ExecutionId) for this many seconds; the next DCL value /// change for that attribute — whatever the value — is published stamped with that /// SourceExecutionId and Trace, and the arm is consumed. An arm that expires /// unconsumed emits nothing. Only traced writes arm, so production behaviour is unchanged. /// Default: 30. /// public int TraceArmWindowSeconds { get; set; } = 30; /// /// Flow Debugger step logs (design 2026-09-09 D7): the maximum number of ScriptLog /// trace rows ONE script run may produce — the author's own Log.* lines plus the /// runtime lines captured while the run is traced. Lines past the cap are counted and /// dropped, and the run emits one closing warn row naming the dropped count, so a /// chatty script degrades to a truncated log instead of an unbounded row stream. /// /// The cap is per RUN, not per script: an inline shared script shares its caller's /// logger and therefore its counter. It only bounds trace rows — every line still reaches /// the node's ILogger, traced or not — and an untraced run produces no rows at all. /// Must be greater than 0. Default: 200. /// public int MaxLogLinesPerRun { get; set; } = 200; }