feat(scripting): out-of-process pre-emptible script runner behind Scripting:Runner (lmxopcua#780 P1)
v2-ci / unit-tests (push) Waiting to run
v2-ci / unit-tests (push) Waiting to run
Opt-in (Scripting:Runner=OutOfProcess; InProcess stays the default and its code path is unchanged). Implements the P1 slice of docs/plans/2026-11-09-out-of-process-script-runner-design.md with the owner's §14 decisions: - Options: Scripting:Runner (InProcess | Shadow | OutOfProcess), Scripting:Runner:Seams, Scripting:Worker:* (Shards 2, HardGrace max(1 s,T), HeapLimitBytes 512 MiB, QuarantineAfterKills 2, UnavailableGrace 5 s, IsolationProbationRuns 20, PeCacheDir validated absolute + outside the content root (fail closed), ExecutablePath). Shadow is not implemented yet: runs InProcess with a startup Warning. - Core.Scripting: IScriptRunner seam + InProcessScriptRunner (today's guard, the parity baseline). - Core.Scripting.Protocol: protobuf envelopes, 4-byte LE framing (zero/oversize/truncated/garbage refused), Variant carrying the exact CLR type; unsupported types are explicit failures. - ScriptWorker app: compiles in the worker, P-thread scheduler, host-driven soft cancel, fail-fast watchdog, heartbeats, ctx.Logger forwarding, exits on pipe EOF / parent gone, forbidden-assembly self-check; Akka and Grpc.Core.Api excluded from its folder. Shipped in script-worker/ (Build + Publish). - Core.Scripting.OutOfProcess: K shards + isolation worker, soft T / hard T+G kill, attribution (deadline kill = one key; crash = suspects -> isolation; isolation crash = strike), kill-strike quarantine, backoff 250 ms -> 30 s, fail closed, cleared allow-listed env, nonce + pid handshake on a current-user pipe, Windows Job Object. - VirtualTag + Calculation seams use the runner when selected; a runner-side failure (worker restarted / runner unavailable) publishes BadResourceUnavailable (0x80040000) — ScadaBridge#112. Script faults keep BadInternalError. The scripted-alarm seam stays in process (alarms last). Tests: new ZB.MOM.WW.OtOpcUa.ScriptWorker.Tests (Category=ScriptWorker, real processes; green on macOS and on windev/Windows), seam tests in Host/Runtime/Calculation suites; mutation-checked. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -72,6 +72,30 @@ public sealed class ScriptingOptions
|
||||
/// </summary>
|
||||
public double StuckOrphanQuarantineFactor { get; set; } = DefaultStuckOrphanQuarantineFactor;
|
||||
|
||||
/// <summary>
|
||||
/// Where compiled scripts run (<c>Scripting:Runner</c>, lmxopcua#780). Default
|
||||
/// <see cref="ScriptRunnerMode.InProcess"/> — today's path, unchanged, and the rollback for the other modes.
|
||||
/// </summary>
|
||||
public ScriptRunnerMode Runner { get; set; } = ScriptRunnerMode.InProcess;
|
||||
|
||||
/// <summary>
|
||||
/// The seams <see cref="Runner"/> applies to when it is not <see cref="ScriptRunnerMode.InProcess"/>
|
||||
/// (<c>Scripting:Runner:Seams</c>, e.g. <c>["Calculation"]</c>). Null or empty means all three. Bound by
|
||||
/// <c>ScriptingOptionsBinding</c> from the documented key, because <c>Scripting:Runner</c> is itself a value.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ScriptSeam>? RunnerSeams { get; set; }
|
||||
|
||||
/// <summary>The out-of-process worker pool's knobs (<c>Scripting:Worker</c>).</summary>
|
||||
public ScriptWorkerOptions Worker { get; set; } = new();
|
||||
|
||||
/// <summary>Whether <paramref name="seam"/> is selected for out-of-process execution by these options.</summary>
|
||||
/// <param name="seam">The seam.</param>
|
||||
/// <returns><c>true</c> when <see cref="Runner"/> is <see cref="ScriptRunnerMode.OutOfProcess"/> and the seam is in
|
||||
/// <see cref="RunnerSeams"/> (or that list is empty).</returns>
|
||||
public bool IsOutOfProcess(ScriptSeam seam)
|
||||
=> Runner == ScriptRunnerMode.OutOfProcess
|
||||
&& (RunnerSeams is not { Count: > 0 } seams || seams.Contains(seam));
|
||||
|
||||
/// <summary>Upper bound <see cref="Sanitized"/> applies to <see cref="StuckOrphanQuarantineFactor"/>.</summary>
|
||||
public const double MaxStuckOrphanQuarantineFactor = 1000;
|
||||
|
||||
@@ -158,6 +182,9 @@ public sealed class ScriptingOptions
|
||||
runTimeout = MaxRunTimeout;
|
||||
}
|
||||
|
||||
var worker = (Worker ?? new ScriptWorkerOptions()).Sanitized(out var workerNotes);
|
||||
notes.AddRange(workerNotes);
|
||||
|
||||
adjustments = notes;
|
||||
return new ScriptingOptions
|
||||
{
|
||||
@@ -165,6 +192,9 @@ public sealed class ScriptingOptions
|
||||
MaxAllocBytesPerRun = alloc,
|
||||
QuarantineAfterConsecutiveTimeouts = threshold,
|
||||
StuckOrphanQuarantineFactor = factor,
|
||||
Runner = Runner,
|
||||
RunnerSeams = RunnerSeams is null ? null : [.. RunnerSeams],
|
||||
Worker = worker,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user