namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// Node-wide script runtime-safety knobs (the Scripting appsettings section), consumed by /// . One instance is shared by every script seam on the node — the /// virtual-tag evaluator, the Calculation evaluator and the scripted-alarm engine — so a single setting /// bounds every authored script the same way. /// /// /// /// The in-process sandbox cannot pre-empt a script: a while(true) is abandoned on its pool /// thread at the timeout, and an allocation is observed only after the run returns. These knobs bound /// the damage — one orphan per script, no further runs until the source changes — not the /// first occurrence. Pre-emptive CPU/memory budgeting needs an out-of-process script runner. /// /// public sealed class ScriptingOptions { /// The appsettings section this binds from. public const string SectionName = "Scripting"; /// Default : 64 MB. public const long DefaultMaxAllocBytesPerRun = 64L * 1024 * 1024; /// Default : 3. public const int DefaultQuarantineAfterConsecutiveTimeouts = 3; /// Default : 10. public const double DefaultStuckOrphanQuarantineFactor = 10; /// /// Managed bytes one script run may allocate on its own thread (sampled with /// around the run) before the script is /// quarantined. Zero or negative disables the budget. Default 64 MB. /// public long MaxAllocBytesPerRun { get; set; } = DefaultMaxAllocBytesPerRun; /// /// Consecutive timeouts after which the script is quarantined: no further runs are dispatched, every /// evaluation reports Bad, and the quarantine lifts only on a source change, a redeploy or an explicit /// reset. Only a timeout is a strike — a run refused because the previous one is still executing is /// reported Bad and counted, but neither adds to nor resets the streak; a completed run (value or /// throw) resets it. Default 3; values below 1 are treated as 1. /// public int QuarantineAfterConsecutiveTimeouts { get; set; } = DefaultQuarantineAfterConsecutiveTimeouts; /// /// A refusal quarantines the script immediately when the abandoned run it was refused behind has been /// executing for longer than this many times the script's timeout (20 s at the 2 s default). This is /// what quarantines a permanently stuck script (while(true)): it times out once and is then /// only ever refused, so the timeout streak alone would never reach /// . A young orphan (a short overrun) is still just a /// refusal. Zero or negative disables the rule. Default 10. /// public double StuckOrphanQuarantineFactor { get; set; } = DefaultStuckOrphanQuarantineFactor; /// Upper bound applies to . public const double MaxStuckOrphanQuarantineFactor = 1000; /// Upper bound applies to . public const int MaxQuarantineAfterConsecutiveTimeouts = 1000; /// /// Lower bound applies to a positive : 1 MB. A /// smaller budget would quarantine practically every script on its first run. /// public const long MinMaxAllocBytesPerRun = 1024L * 1024; /// /// Returns a copy with every numeric knob pulled into a sane range. The bind site calls it and logs /// . calls it again, so options that reach the /// guard by another route (a Calculation driver's own config, a test) are clamped too. /// /// : NaN becomes the default, a negative value becomes 0 /// (disabled), and a value above becomes that maximum. /// Unclamped, an absurd factor made timeout * factor throw on /// the first refused run. /// : into [1, ]. /// : a negative value becomes 0 (disabled), and a positive value /// below becomes that minimum. /// /// /// One line per value that was out of range, naming its key. A negative value that /// already meant "disabled" is normalised without a line. /// A new, clamped instance. This instance is not modified. public ScriptingOptions Sanitized(out IReadOnlyList adjustments) { var notes = new List(); var factor = StuckOrphanQuarantineFactor; if (double.IsNaN(factor)) { notes.Add($"{SectionName}:{nameof(StuckOrphanQuarantineFactor)} is NaN; using the default {DefaultStuckOrphanQuarantineFactor}."); factor = DefaultStuckOrphanQuarantineFactor; } else if (factor < 0) { factor = 0; } else if (factor > MaxStuckOrphanQuarantineFactor) { notes.Add($"{SectionName}:{nameof(StuckOrphanQuarantineFactor)} {factor} is above the maximum; clamped to {MaxStuckOrphanQuarantineFactor}."); factor = MaxStuckOrphanQuarantineFactor; } var threshold = QuarantineAfterConsecutiveTimeouts; if (threshold < 1) { notes.Add($"{SectionName}:{nameof(QuarantineAfterConsecutiveTimeouts)} {threshold} is below 1; clamped to 1."); threshold = 1; } else if (threshold > MaxQuarantineAfterConsecutiveTimeouts) { notes.Add($"{SectionName}:{nameof(QuarantineAfterConsecutiveTimeouts)} {threshold} is above the maximum; clamped to {MaxQuarantineAfterConsecutiveTimeouts}."); threshold = MaxQuarantineAfterConsecutiveTimeouts; } var alloc = MaxAllocBytesPerRun; if (alloc < 0) { alloc = 0; } else if (alloc > 0 && alloc < MinMaxAllocBytesPerRun) { notes.Add($"{SectionName}:{nameof(MaxAllocBytesPerRun)} {alloc} is below the minimum; clamped to {MinMaxAllocBytesPerRun}."); alloc = MinMaxAllocBytesPerRun; } adjustments = notes; return new ScriptingOptions { MaxAllocBytesPerRun = alloc, QuarantineAfterConsecutiveTimeouts = threshold, StuckOrphanQuarantineFactor = factor, }; } }