7fd44f0fa7
U2 (Critical): RoslynVirtualTagEvaluator ran scripts with a CancellationToken that a synchronous Roslyn script can never observe, so one CPU-bound/infinite- loop virtual-tag script hung the owning VirtualTagActor forever (no timeout, no recovery). Route evaluation through the existing TimedScriptEvaluator (Task.Run + WaitAsync) so the wall-clock budget is real; a runaway script now returns Bad within the timeout instead of wedging the actor. U3 (High): the live path cached evaluators in a raw ConcurrentDictionary never cleared on republish, leaking a collectible AssemblyLoadContext per edited script forever. Swap in the purpose-built CompiledScriptCache (Lazy single- compile, dispose-on-Clear) and add an apply-boundary clear: a new narrow IScriptCacheOwner seam the VirtualTagHostActor calls on each ApplyVirtualTags generation (mirrors ScriptedAlarmEngine's per-generation clear). Tests: infinite-loop-fails-within-timeout (bounded by WaitAsync so a regression fails instead of hanging), happy-path-after-wrapping, ClearCompiledScripts- empties-cache, and a host-actor wiring guard asserting each apply generation calls ClearCompiledScripts (theme #1 production-caller assertion). Host.Integration VT suite 18/18, Runtime VT suite 12/12.
21 lines
1.2 KiB
C#
21 lines
1.2 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
|
|
|
/// <summary>
|
|
/// Optional capability an <see cref="IVirtualTagEvaluator"/> may implement when it caches
|
|
/// compiled scripts and needs an apply-boundary drop. The production
|
|
/// <c>RoslynVirtualTagEvaluator</c> caches one collectible
|
|
/// <see cref="System.Runtime.Loader.AssemblyLoadContext"/> per compiled expression; without a
|
|
/// drop on each config-apply generation those ALCs accrete forever across script edits (every
|
|
/// republish roots a new one). <c>VirtualTagHostActor</c> casts its injected evaluator to this
|
|
/// interface and calls <see cref="ClearCompiledScripts"/> at each apply so stale ALCs are
|
|
/// reclaimed — mirroring how <c>ScriptedAlarmEngine</c> clears its compile cache per generation.
|
|
/// Evaluators with no cache (e.g. <see cref="NullVirtualTagEvaluator"/>) simply don't implement
|
|
/// it, and the host actor's null-conditional cast makes the call a no-op.
|
|
/// </summary>
|
|
public interface IScriptCacheOwner
|
|
{
|
|
/// <summary>Drops every cached compiled script, disposing each so its collectible
|
|
/// AssemblyLoadContext unloads. Called at each config-apply generation boundary.</summary>
|
|
void ClearCompiledScripts();
|
|
}
|