fix(scripting): give scripted-alarm predicates the configured run budget, not 250 ms (lmxopcua#779)
DriverHostActor built ScriptedAlarmEngine without a scriptTimeout, so every
production alarm predicate fell back to TimedScriptEvaluator.DefaultTimeout
(250 ms) while virtual tags and Calculation tags ran on 2 s. History shows no
decision behind it: 250 ms is the generic Phase-7 A.4 default, the VT
evaluator moved to 2 s in F8b (219d10a2) and the alarm engine was simply never
given a value.
- New node-wide Scripting:RunTimeout (default 2 s, clamped to (0, 60 s],
non-positive -> default, each clamp a startup Warning) on ScriptingOptions.
- The VT evaluator and the alarm engine default to it; DriverHostActor now
builds the engine through CreateScriptedAlarmEngine, which passes it
explicitly. Calculation keeps its per-driver runTimeoutMs.
- Docs: ScriptEditor (config table), ScriptedAlarms, VirtualTags.
Tests: ScriptedAlarmEngineBudgetTests (a 600 ms predicate now completes and
activates; default and configured budgets), ScriptedAlarmEngineBudgetWiringTests
(the host-built engine), ScriptingOptionsBindingTests (bind + clamps). The
engine and wiring tests fail with the old 250 ms default restored.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -19,6 +19,12 @@ public sealed class ScriptingOptions
|
||||
/// <summary>The appsettings section this binds from.</summary>
|
||||
public const string SectionName = "Scripting";
|
||||
|
||||
/// <summary>Default <see cref="RunTimeout"/>: 2 s — the budget virtual tags and Calculation tags have always run with.</summary>
|
||||
public static readonly TimeSpan DefaultRunTimeout = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>Upper bound <see cref="Sanitized"/> applies to <see cref="RunTimeout"/>: 60 s.</summary>
|
||||
public static readonly TimeSpan MaxRunTimeout = TimeSpan.FromSeconds(60);
|
||||
|
||||
/// <summary>Default <see cref="MaxAllocBytesPerRun"/>: 64 MB.</summary>
|
||||
public const long DefaultMaxAllocBytesPerRun = 64L * 1024 * 1024;
|
||||
|
||||
@@ -28,6 +34,18 @@ public sealed class ScriptingOptions
|
||||
/// <summary>Default <see cref="StuckOrphanQuarantineFactor"/>: 10.</summary>
|
||||
public const double DefaultStuckOrphanQuarantineFactor = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Wall-clock budget for one script run on the node-wide seams: virtual tags and scripted-alarm
|
||||
/// predicates. Default 2 s. A Calculation driver keeps its own per-driver <c>runTimeoutMs</c> (default
|
||||
/// 2 s as well). Before lmxopcua#779 the scripted-alarm engine was built without a budget and silently
|
||||
/// took the 250 ms Phase-7 default of <see cref="TimedScriptEvaluator{TContext, TResult}"/>, so an alarm
|
||||
/// predicate got an eighth of the budget a virtual tag reading the same inputs got — not a decision
|
||||
/// anywhere in the history, just a missing argument. Also scales the stuck-orphan rule
|
||||
/// (<see cref="StuckOrphanQuarantineFactor"/> × this). Zero or negative becomes the default; above
|
||||
/// <see cref="MaxRunTimeout"/> becomes that maximum.
|
||||
/// </summary>
|
||||
public TimeSpan RunTimeout { get; set; } = DefaultRunTimeout;
|
||||
|
||||
/// <summary>
|
||||
/// Managed bytes one script run may allocate on its own thread (sampled with
|
||||
/// <see cref="GC.GetAllocatedBytesForCurrentThread"/> around the run) before the script is
|
||||
@@ -78,6 +96,8 @@ public sealed class ScriptingOptions
|
||||
/// <item><see cref="QuarantineAfterConsecutiveTimeouts"/>: into [1, <see cref="MaxQuarantineAfterConsecutiveTimeouts"/>].</item>
|
||||
/// <item><see cref="MaxAllocBytesPerRun"/>: a negative value becomes 0 (disabled), and a positive value
|
||||
/// below <see cref="MinMaxAllocBytesPerRun"/> becomes that minimum.</item>
|
||||
/// <item><see cref="RunTimeout"/>: zero or negative becomes <see cref="DefaultRunTimeout"/>, above
|
||||
/// <see cref="MaxRunTimeout"/> becomes that maximum.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="adjustments">One line per value that was out of range, naming its key. A negative value that
|
||||
@@ -126,9 +146,22 @@ public sealed class ScriptingOptions
|
||||
alloc = MinMaxAllocBytesPerRun;
|
||||
}
|
||||
|
||||
var runTimeout = RunTimeout;
|
||||
if (runTimeout <= TimeSpan.Zero)
|
||||
{
|
||||
notes.Add($"{SectionName}:{nameof(RunTimeout)} {runTimeout} is not positive; using the default {DefaultRunTimeout}.");
|
||||
runTimeout = DefaultRunTimeout;
|
||||
}
|
||||
else if (runTimeout > MaxRunTimeout)
|
||||
{
|
||||
notes.Add($"{SectionName}:{nameof(RunTimeout)} {runTimeout} is above the maximum; clamped to {MaxRunTimeout}.");
|
||||
runTimeout = MaxRunTimeout;
|
||||
}
|
||||
|
||||
adjustments = notes;
|
||||
return new ScriptingOptions
|
||||
{
|
||||
RunTimeout = runTimeout,
|
||||
MaxAllocBytesPerRun = alloc,
|
||||
QuarantineAfterConsecutiveTimeouts = threshold,
|
||||
StuckOrphanQuarantineFactor = factor,
|
||||
|
||||
Reference in New Issue
Block a user