261 lines
14 KiB
C#
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;
|
|
}
|