Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/SiteRuntimeOptions.cs
T

261 lines
14 KiB
C#

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime;
/// <summary>
/// Configuration options for the Site Runtime component.
/// Bound from ScadaBridge:SiteRuntime configuration section.
/// </summary>
public class SiteRuntimeOptions
{
/// <summary>
/// Number of Instance Actors to create per batch during staggered startup.
/// Default: 20.
/// </summary>
public int StartupBatchSize { get; set; } = 20;
/// <summary>
/// Delay in milliseconds between startup batches to prevent reconnection storms.
/// Default: 100ms.
/// </summary>
public int StartupBatchDelayMs { get; set; } = 100;
/// <summary>
/// Maximum call depth for recursive script calls (CallScript/CallShared).
/// Default: 10.
/// </summary>
public int MaxScriptCallDepth { get; set; } = 10;
/// <summary>
/// Default script execution timeout in seconds.
/// Default: 30 seconds.
/// </summary>
public int ScriptExecutionTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Per-subscriber buffer size for the site-wide Akka stream.
/// Slow subscribers drop oldest messages when buffer is full.
/// Default: 1000.
/// </summary>
public int StreamBufferSize { get; set; } = 1000;
/// <summary>
/// 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.
///
/// <para>WP3.1: this was a fixed size and is now the lower bound of a grow-only,
/// instance-scaled pool — see
/// <see cref="Scripts.ScriptExecutionScheduler.ComputeTargetThreads"/>. Existing
/// configurations keep exactly the previous behaviour at or below
/// <c>ScriptExecutionThreadCount * InstancesPerScriptThread</c> deployed instances.</para>
///
/// Default: 8.
/// </summary>
public int ScriptExecutionThreadCount { get; set; } = 8;
/// <summary>
/// WP3.1: CEILING for the instance-scaled script-execution pool. The pool grows
/// towards <c>ceil(enabledInstances / 8)</c> threads but never past this value; beyond
/// it, <see cref="MaxConcurrentRunsPerScript"/> 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 <see cref="ScriptExecutionThreadCount"/>.
/// Default: 32.
/// </summary>
public int ScriptExecutionMaxThreadCount { get; set; } = 32;
/// <summary>
/// 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 (<see cref="Scripts.TriggerExpressionGlobals"/> 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: <c>max(2, Environment.ProcessorCount)</c>.
/// </summary>
public int TriggerEvalMaxConcurrency { get; set; } = Math.Max(2, Environment.ProcessorCount);
/// <summary>
/// WP3.1: timeout (seconds) for a single trigger-expression evaluation. Previously
/// hardcoded at 2 s in both <c>ScriptActor</c> and <c>AlarmActor</c>. The deadline is now
/// armed when the evaluation is ENQUEUED, not when it is dequeued, so time spent waiting
/// on <see cref="TriggerEvalMaxConcurrency"/> burns the same budget — a saturated gate
/// produces a timely "false" instead of an unbounded stall.
/// Default: 2.
/// </summary>
public int TriggerEvalTimeoutSeconds { get; set; } = 2;
/// <summary>
/// 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
/// <c>ISiteHealthCollector.IncrementScriptRunShed</c>, emits a rate-limited Warning site
/// event, and — for an Ask-based <c>CallScript</c> — replies with an explicit error rather
/// than letting the caller hang to its Ask timeout.
/// Default: 4.
/// </summary>
public int MaxConcurrentRunsPerScript { get; set; } = 4;
/// <summary>
/// Max mirrored native alarms retained per source binding before older entries are dropped (logged).
/// Default: 1000.
/// </summary>
public int MirroredAlarmCapPerSource { get; set; } = 1000;
/// <summary>
/// Interval in milliseconds to retry a failed native alarm subscription.
/// Default: 5000ms.
/// </summary>
public int NativeAlarmRetryIntervalMs { get; set; } = 5000;
/// <summary>HTTP timeout (seconds) for fetching a deployment config from central (notify-and-fetch).</summary>
public int ConfigFetchTimeoutSeconds { get; set; } = 30;
/// <summary>
/// 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
/// <see cref="TagSubscribeRetryMaxIntervalMs"/>, and a Success or a connection
/// returning to Good quality resets it here. Default: 5000ms.
/// </summary>
public int TagSubscribeRetryIntervalMs { get; set; } = 5000;
/// <summary>
/// Ceiling (ms) of the tag-subscribe retry backoff. Must be >=
/// <see cref="TagSubscribeRetryIntervalMs"/>; equal makes the interval fixed
/// (pre-Lever-1 behaviour). Default: 60000ms.
/// </summary>
public int TagSubscribeRetryMaxIntervalMs { get; set; } = 60000;
/// <summary>
/// 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.
/// <para>
/// 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.
/// </para>
/// </summary>
public int TagSubscribeInFlightCeilingMs { get; set; } = 45000;
/// <summary>
/// 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.
/// </summary>
public int StuckScriptGraceMs { get; set; } = 30000;
/// <summary>
/// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer):
/// capacity of the bounded hand-off queue feeding <see cref="Streaming.SiteStreamManager"/>'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
/// <see cref="StreamBufferSize"/>).
/// </summary>
public int AlarmPublishQueueCapacity { get; set; } = 2000;
/// <summary>
/// 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
/// <c>site_events</c> 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 (<see cref="Scripts.ScriptRunSummaryRecorder"/>) replaces the per-run rows
/// and script Error events (timeout/failure/stuck-watchdog) are unaffected — they are
/// unconditional regardless of this switch. Read via
/// <see cref="Microsoft.Extensions.Options.IOptionsMonitor{TOptions}"/> at the top of
/// every run, so it is hot-togglable (no restart, no redeploy). Default: false.
/// </summary>
public bool PerRunScriptEvents { get; set; }
/// <summary>
/// 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
/// <see cref="PerRunScriptEvents"/> switch (and therefore without paying its volume
/// cost for every other script). Effective only while the global switch is off.
/// Entries are <c>"{InstanceName}/{ScriptName}"</c> for an exact match, or
/// <c>"{InstanceName}/*"</c> to opt in every script on that instance. Matched by
/// <see cref="Scripts.ScriptRunEventPolicy.ShouldEmitPerRun"/>, read from
/// <see cref="Microsoft.Extensions.Options.IOptionsMonitor{TOptions}"/> per run — hot
/// -togglable like <see cref="PerRunScriptEvents"/>. Default: empty (no opt-ins).
/// </summary>
public List<string> PerRunScriptEventScripts { get; set; } = new();
/// <summary>
/// WP3.2: flush interval (seconds) for <see cref="Scripts.ScriptRunSummaryRecorder"/>'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. <c>0</c> 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.
/// </summary>
public int ScriptRunSummaryIntervalSeconds { get; set; } = 300;
/// <summary>
/// Interval (seconds) of the Deployment Manager singleton's <b>instance reconcile sweep</b>
/// (deferred register rows 78 + 72). Every tick the singleton re-reads the durable
/// <c>deployed_configurations</c> rows and repairs two silent divergences between the
/// stored configs and the running Instance Actors:
/// <list type="bullet">
/// <item>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);</item>
/// <item>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).</item>
/// </list>
/// The sweep is silent when there is nothing to repair. <c>0</c> disables it entirely
/// (the row-67 <c>EnsureInstanceStartedCommand</c> path is unaffected). Default: 60.
/// </summary>
public int InstanceSweepIntervalSeconds { get; set; } = 60;
/// <summary>
/// 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
/// <c>(attribute → writer's ExecutionId)</c> for this many seconds; the next DCL value
/// change for that attribute — whatever the value — is published stamped with that
/// <c>SourceExecutionId</c> and <c>Trace</c>, and the arm is consumed. An arm that expires
/// unconsumed emits nothing. Only traced writes arm, so production behaviour is unchanged.
/// Default: 30.
/// </summary>
public int TraceArmWindowSeconds { get; set; } = 30;
/// <summary>
/// Flow Debugger step logs (design 2026-09-09 D7): the maximum number of <c>ScriptLog</c>
/// trace rows ONE script run may produce — the author's own <c>Log.*</c> 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 <c>warn</c> row naming the dropped count, so a
/// chatty script degrades to a truncated log instead of an unbounded row stream.
///
/// <para>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 <c>ILogger</c>, traced or not — and an untraced run produces no rows at all.
/// Must be greater than 0. Default: 200.</para>
/// </summary>
public int MaxLogLinesPerRun { get; set; } = 200;
}