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:
+61
@@ -0,0 +1,61 @@
|
||||
using Serilog;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// lmxopcua#779 — an engine built without an explicit <c>scriptTimeout</c> (which is how the host built every
|
||||
/// production engine) ran its predicates on the 250 ms Phase-7 default instead of the 2 s budget virtual tags
|
||||
/// and Calculation tags get. The default is now <see cref="ScriptingOptions.RunTimeout"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ScriptedAlarmEngineBudgetTests
|
||||
{
|
||||
/// <summary>A predicate that busy-waits ~600 ms and then fires: over the old 250 ms budget, well inside 2 s.</summary>
|
||||
private const string SixHundredMsPredicate =
|
||||
"""var t = ctx.GetTag("Temp"); var end = System.DateTime.UtcNow.AddMilliseconds(600); while (System.DateTime.UtcNow < end) { } return true;""";
|
||||
|
||||
private static ScriptedAlarmEngine Build(FakeUpstream up, ScriptingOptions? scripting = null)
|
||||
{
|
||||
var logger = new LoggerConfiguration().CreateLogger();
|
||||
return new ScriptedAlarmEngine(up, new InMemoryAlarmStateStore(), new ScriptLoggerFactory(logger), logger,
|
||||
scripting: scripting);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Without_an_explicit_timeout_the_budget_is_the_Scripting_RunTimeout_default_not_250ms()
|
||||
{
|
||||
using var eng = Build(new FakeUpstream());
|
||||
eng.ScriptTimeout.ShouldBe(ScriptingOptions.DefaultRunTimeout);
|
||||
eng.ScriptTimeout.ShouldBe(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_configured_RunTimeout_is_the_budget()
|
||||
{
|
||||
using var eng = Build(new FakeUpstream(), new ScriptingOptions { RunTimeout = TimeSpan.FromSeconds(7) });
|
||||
eng.ScriptTimeout.ShouldBe(TimeSpan.FromSeconds(7));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_predicate_slower_than_250ms_but_inside_the_budget_completes_and_activates()
|
||||
{
|
||||
var up = new FakeUpstream();
|
||||
up.Set("Temp", 50);
|
||||
using var eng = Build(up);
|
||||
|
||||
await eng.LoadAsync(
|
||||
[new ScriptedAlarmDefinition(AlarmId: "slow", EquipmentPath: "Plant/Line1/Reactor", AlarmName: "slow",
|
||||
Kind: AlarmKind.AlarmCondition, Severity: AlarmSeverity.High, MessageTemplate: "m",
|
||||
PredicateScriptSource: SixHundredMsPredicate)],
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// LoadAsync evaluates once (startup recovery). Under the old 250 ms default that run timed out and the
|
||||
// condition stayed Inactive with a Bad annotation.
|
||||
eng.RunGuard.StartedRunCount.ShouldBe(1);
|
||||
eng.GetState("slow")!.Active.ShouldBe(AlarmActiveState.Active);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user