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,
};
}
}