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