using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// The execution seam every host script owner can run a compiled script through (lmxopcua#780, design §11): a /// script key, its source, its input snapshots and its budget go in; one comes /// out. Implementations never throw for script faults. /// /// /// /// is today's + /// behind this interface (the parity baseline). The out-of-process runner /// (ZB.MOM.WW.OtOpcUa.Core.Scripting.Runner) runs the same compile in a pre-emptible worker process. /// The passthrough fast path (PassthroughScript) never reaches a runner: it runs no user code. /// /// /// Keys are namespaced by , so a virtual-tag id and a calc RawPath that /// happen to be equal never share quarantine state, and for one seam leaves the other /// seams' quarantines alone. /// /// public interface IScriptRunner { /// Runs one compiled evaluation. /// What to run. /// Caller cancellation (shutdown); wins over the budget. /// The outcome; never faults for a script failure. Task RunAsync(ScriptInvocation invocation, CancellationToken ct = default); /// /// Submits several evaluations at once (design §4.5): the out-of-process runner sends every one that is ready to /// run as ONE request frame per worker, instead of one frame and one submission per evaluation. Results are /// per item and streamed — each task completes when its own evaluation does, so a slow item never holds a fast /// sibling's result — and every item keeps exactly the semantics of : its own budget, its /// own single-flight slot, and its own blame (a runaway item is killed and struck alone; an item queued behind /// it is neither timed out nor struck for its time). /// /// /// The default implementation — what uses — is per /// item, started together. A key that appears twice in one batch is refused for its second item exactly as two /// concurrent calls would be; callers that may repeat a key split the batch there. /// Never throws for script faults; the returned tasks never fault. /// /// What to run, in order. /// Caller cancellation (shutdown); wins over every item's budget. /// One task per invocation, in the same order. IReadOnlyList> RunBatch(IReadOnlyList invocations, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(invocations); var outcomes = new Task[invocations.Count]; for (var i = 0; i < outcomes.Length; i++) outcomes[i] = RunAsync(invocations[i], ct); return outcomes; } /// /// The deploy boundary for one seam: lifts every quarantine and forgets every remembered compile failure of /// , and drops its compiled scripts. /// /// The seam's context kind. void Reset(ScriptContextKind kind); /// The keys of currently quarantined. /// The seam's context kind. /// The quarantined keys (un-namespaced). IReadOnlyCollection QuarantinedKeys(ScriptContextKind kind); } /// Which script context (and result type) a script compiles against. public enum ScriptContextKind { /// A virtual tag: VirtualTagContext → object?. VirtualTag = 1, /// A Calculation tag: VirtualTagContext → object?. Calculation = 2, /// A scripted-alarm predicate: AlarmPredicateContext → bool. AlarmPredicate = 3, } /// One evaluation handed to an . /// The script instance key (virtual-tag id, calc RawPath, alarm id). /// The context kind. /// The script source (also its identity: a change lifts the key's quarantine). /// The read cache: dependency snapshots by path. Not mutated until the run completes. /// The soft budget T. /// The per-evaluation script logger ctx.Logger writes through; null ⇒ a silent logger. public sealed record ScriptInvocation( string Key, ScriptContextKind Kind, string Source, IReadOnlyDictionary Inputs, TimeSpan Timeout, Serilog.ILogger? ScriptLogger = null); /// How an evaluation ended. public enum ScriptRunStatus { /// The script returned . Ok = 0, /// A script-attributable failure (threw, timed out, refused, quarantined, did not compile) — Bad as today. Failed = 1, /// /// The runner could not run the script, and it is not the script's fault: its worker restarted under it, or no /// worker became ready in time. Published as BadResourceUnavailable (design §9 / §14 #3). /// Unavailable = 2, } /// The result of one . /// How it ended. /// The script's result when . /// The failure reason otherwise; the same strings the evaluators have always published. public sealed record ScriptRunOutcome(ScriptRunStatus Status, object? Value, string? Reason) { /// Reason when the worker running the script died under it (another script's kill, a crash). public const string WorkerRestartedReason = "script worker restarted"; /// Reason when no worker became ready within . public const string RunnerUnavailableReason = "script runner unavailable"; /// Reason when the worker could not take more work (the out-of-process runner's back-pressure answer). public const string WorkerBusyReason = "script worker busy"; /// /// Reason when the worker neither answered nor reported the script over its budget within T + G of submit — the /// pipe or a starved / stalled worker process, not the script (lmxopcua#827). /// public const string WorkerUnresponsiveReason = "script worker unresponsive"; /// /// For : completes when the runner can run this script again (its worker /// is back), so the seam re-evaluates it then with its latest inputs (design §9, "re-evaluated on the shard's /// return"). Null when there is nothing to wait for. /// public Task? Recovery { get; init; } /// A successful run. /// The script's result. /// The outcome. public static ScriptRunOutcome Ok(object? value) => new(ScriptRunStatus.Ok, value, null); /// A script-attributable failure. /// The reason. /// The outcome. public static ScriptRunOutcome Failed(string reason) => new(ScriptRunStatus.Failed, null, reason); /// A runner-attributable failure. /// The reason. /// The outcome. public static ScriptRunOutcome Unavailable(string reason) => new(ScriptRunStatus.Unavailable, null, reason); } /// Maps an outcome onto the value seams' result type. public static class ScriptRunOutcomeExtensions { /// /// → Ok(value); → Failure(reason) /// (published BadInternalError, as today); → /// Unavailable(reason) (published BadResourceUnavailable). /// /// The runner's outcome. /// The seam result. public static Commons.Engines.VirtualTagEvalResult ToVirtualTagEvalResult(this ScriptRunOutcome outcome) { ArgumentNullException.ThrowIfNull(outcome); return outcome.Status switch { ScriptRunStatus.Ok => Commons.Engines.VirtualTagEvalResult.Ok(outcome.Value), ScriptRunStatus.Unavailable => Commons.Engines.VirtualTagEvalResult.Unavailable( outcome.Reason ?? ScriptRunOutcome.RunnerUnavailableReason) with { RetryWhen = outcome.Recovery }, _ => Commons.Engines.VirtualTagEvalResult.Failure(outcome.Reason ?? "evaluator failure"), }; } } /// /// Which each script seam uses on this node (lmxopcua#780), for components that cannot /// reference the Host's selection directly (the Runtime's scripted-alarm host). Null for a seam means its in-process /// guard path. /// public interface IScriptRunnerProvider { /// /// The runner must use, or null for its in-process path. Under /// Scripting:Runner=Shadow this is a : the seam recognises it as an /// , keeps its in-process path and mirrors each evaluation to it. /// /// The seam. /// The runner, or null. IScriptRunner? ForSeam(ScriptSeam seam); }