fix(virtual-tags): evaluate scripts off the Akka dispatcher thread (lmxopcua#779)
VirtualTagActor called the blocking IVirtualTagEvaluator.Evaluate, and RoslynVirtualTagEvaluator blocked on the run guard with .GetAwaiter().GetResult(), so every script run parked a dispatcher thread for up to its 2 s budget (a first-use Roslyn compile did too). This is the #750 P0 refactor for virtual tags. - IVirtualTagEvaluator.EvaluateAsync (default: a completed task over the snapshot overload, so the null evaluator and fakes are unchanged). - RoslynVirtualTagEvaluator implements it without blocking: passthrough and compile failures complete synchronously, a first-use compile goes to the pool, the run is awaited. The blocking overload now delegates to it. - VirtualTagActor: a completed task is applied in the same turn, as before; an incomplete one switches to an "evaluating" behaviour that stashes every dependency/reassert message until a private completion message arrives, then applies the result and unstashes. Each trigger is still evaluated against its own inputs, results are applied/published in trigger order and the dedup/Bad-transition state sees the same sequence. The dependency map is not touched while a run reads it. Calculation and scripted-alarm serialisation is NOT relaxed: neither blocks a thread, and running independent scripts in parallel is not semantics-neutral (pool consumption bound, cross-tag publish order, timer/pump exclusion for Calc; state-machine/persist boundary, transition timestamps and the store-failure path for alarms). Recorded in docs/VirtualTags.md and the #750 design's P0 entry. Tests: VirtualTagActorAsyncEvaluationTests — on a one-thread dispatcher a slow script no longer starves a sibling actor and the slow actor answers Identify (fails when the actor calls the blocking Evaluate); triggers during a run are evaluated afterwards in order against their own inputs; a reassert during a run is applied after it; a faulted evaluation takes the evaluator-threw path (the ordering/reassert tests fail with the stash removed). RoslynVirtualTagEvaluatorAsyncTests — a slow script and a cold compile return an incomplete task (fail without the override); cheap outcomes and timeouts match the blocking overload. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -47,6 +47,29 @@ public interface IVirtualTagEvaluator
|
||||
foreach (var kv in dependencies) values[kv.Key] = kv.Value?.Value;
|
||||
return Evaluate(virtualTagId, expression, values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous form of the snapshot overload — what <c>VirtualTagActor</c> calls (lmxopcua#779). An
|
||||
/// implementation whose evaluation can take real time (the Roslyn one: a compile on first use, then a
|
||||
/// script run of up to its budget) returns an <b>incomplete</b> task instead of blocking the caller, so
|
||||
/// the actor's dispatcher thread is free while the script runs. The actor applies the result when the
|
||||
/// task completes, in trigger order.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Default-implemented as a synchronously completed task over the snapshot overload, so the null
|
||||
/// evaluator and every test fake keep their exact behaviour: a completed task is applied inline by the
|
||||
/// actor, in the same message turn, as before. Must not fault for script failures — those are
|
||||
/// <see cref="VirtualTagEvalResult.Failure"/>; a faulted task is treated as "evaluator threw".
|
||||
/// <para>The caller must not mutate <paramref name="dependencies"/> until the task completes: the
|
||||
/// implementation reads it on another thread while the script runs.</para>
|
||||
/// </remarks>
|
||||
/// <param name="virtualTagId">The unique identifier of the virtual tag being evaluated.</param>
|
||||
/// <param name="expression">The expression string to evaluate.</param>
|
||||
/// <param name="dependencies">Read-only dictionary of dependency snapshots, keyed by tag path.</param>
|
||||
/// <returns>The evaluation result.</returns>
|
||||
Task<VirtualTagEvalResult> EvaluateAsync(
|
||||
string virtualTagId, string expression, IReadOnlyDictionary<string, DataValueSnapshot> dependencies)
|
||||
=> Task.FromResult(Evaluate(virtualTagId, expression, dependencies));
|
||||
}
|
||||
|
||||
/// <summary>Result of one virtual-tag expression eval. Stash a Reason on every Failure so
|
||||
|
||||
Reference in New Issue
Block a user