125 lines
5.7 KiB
C#
125 lines
5.7 KiB
C#
using System.Collections.Concurrent;
|
|
using Microsoft.Extensions.Logging;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
using SerilogLogger = Serilog.ILogger;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.Engines;
|
|
|
|
/// <summary>
|
|
/// F9b — production <see cref="IScriptedAlarmEvaluator"/> binding. Compiles each unique
|
|
/// predicate once via <see cref="ScriptEvaluator{TContext, TResult}"/> against
|
|
/// <see cref="AlarmPredicateContext"/> and caches the resulting evaluator. Predicates are
|
|
/// pure functions returning <c>bool</c>: <see cref="AlarmPredicateContext.SetVirtualTag"/>
|
|
/// throws so a misbehaving script can't smuggle a side effect into alarm evaluation.
|
|
///
|
|
/// Failure modes (compile error, sandbox violation, runtime exception, timeout) all surface
|
|
/// as <see cref="ScriptedAlarmEvalResult.Failure"/>; <see cref="ScriptedAlarmActor"/>
|
|
/// preserves the prior state on failure (does not flip Active/Inactive).
|
|
/// </summary>
|
|
public sealed class RoslynScriptedAlarmEvaluator : IScriptedAlarmEvaluator, IDisposable
|
|
{
|
|
private readonly ConcurrentDictionary<string, ScriptEvaluator<AlarmPredicateContext, bool>> _cache
|
|
= new(StringComparer.Ordinal);
|
|
private readonly ILogger<RoslynScriptedAlarmEvaluator> _logger;
|
|
private readonly SerilogLogger _scriptRoot;
|
|
private readonly TimeSpan _runTimeout;
|
|
private bool _disposed;
|
|
|
|
/// <summary>Initializes a new instance of the Roslyn scripted alarm evaluator.</summary>
|
|
/// <param name="logger">Logger for diagnostic messages (host diagnostics).</param>
|
|
/// <param name="scriptRoot">Root script logger; user <c>ctx.Logger.*</c> output flows through this to the Script-log page.</param>
|
|
/// <param name="runTimeout">Optional timeout for script evaluation; defaults to 2 seconds.</param>
|
|
public RoslynScriptedAlarmEvaluator(
|
|
ILogger<RoslynScriptedAlarmEvaluator> logger,
|
|
ScriptRootLogger scriptRoot,
|
|
TimeSpan? runTimeout = null)
|
|
{
|
|
_logger = logger;
|
|
_scriptRoot = (scriptRoot ?? throw new ArgumentNullException(nameof(scriptRoot))).Logger;
|
|
_runTimeout = runTimeout ?? TimeSpan.FromSeconds(2);
|
|
}
|
|
|
|
/// <summary>Evaluates a scripted alarm predicate against provided dependencies.</summary>
|
|
/// <param name="alarmId">The alarm identifier for logging purposes.</param>
|
|
/// <param name="predicate">The predicate expression to evaluate.</param>
|
|
/// <param name="dependencies">Variables available to the predicate expression.</param>
|
|
/// <returns>Evaluation result with success flag and active state or failure reason.</returns>
|
|
public ScriptedAlarmEvalResult Evaluate(string alarmId, string predicate, IReadOnlyDictionary<string, object?> dependencies)
|
|
{
|
|
if (_disposed) return ScriptedAlarmEvalResult.Failure("evaluator disposed");
|
|
if (string.IsNullOrWhiteSpace(predicate)) return ScriptedAlarmEvalResult.Failure("empty predicate");
|
|
|
|
ScriptEvaluator<AlarmPredicateContext, bool> evaluator;
|
|
try
|
|
{
|
|
evaluator = _cache.GetOrAdd(predicate, ScriptEvaluator<AlarmPredicateContext, bool>.Compile);
|
|
}
|
|
catch (CompilationErrorException ex)
|
|
{
|
|
_logger.LogWarning(ex, "Alarm {Id}: predicate compile failed", alarmId);
|
|
return ScriptedAlarmEvalResult.Failure($"compile error: {ex.Message}");
|
|
}
|
|
catch (ScriptSandboxViolationException ex)
|
|
{
|
|
_logger.LogWarning(ex, "Alarm {Id}: predicate sandbox violation", alarmId);
|
|
return ScriptedAlarmEvalResult.Failure($"sandbox violation: {ex.Message}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Alarm {Id}: predicate compile threw", alarmId);
|
|
return ScriptedAlarmEvalResult.Failure($"compile failure: {ex.Message}");
|
|
}
|
|
|
|
var readCache = BuildReadCache(dependencies);
|
|
// Per-evaluation script logger: bind both ScriptId and AlarmId from the alarm id so the
|
|
// Script-log page can attribute each line to the owning scripted alarm.
|
|
var scriptLog = _scriptRoot
|
|
.ForContext(ScriptLoggerFactory.ScriptIdProperty, alarmId)
|
|
.ForContext(ScriptLoggerFactory.AlarmIdProperty, alarmId);
|
|
var context = new AlarmPredicateContext(readCache, scriptLog);
|
|
|
|
try
|
|
{
|
|
using var cts = new CancellationTokenSource(_runTimeout);
|
|
var active = evaluator.RunAsync(context, cts.Token).GetAwaiter().GetResult();
|
|
return ScriptedAlarmEvalResult.Ok(active);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return ScriptedAlarmEvalResult.Failure($"predicate timed out after {_runTimeout.TotalSeconds:F1}s");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Alarm {Id}: predicate execution threw", alarmId);
|
|
return ScriptedAlarmEvalResult.Failure($"predicate threw: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, DataValueSnapshot> BuildReadCache(
|
|
IReadOnlyDictionary<string, object?> deps)
|
|
{
|
|
var nowUtc = DateTime.UtcNow;
|
|
var cache = new Dictionary<string, DataValueSnapshot>(StringComparer.Ordinal);
|
|
foreach (var kv in deps)
|
|
{
|
|
cache[kv.Key] = new DataValueSnapshot(kv.Value, StatusCode: 0u, SourceTimestampUtc: nowUtc, ServerTimestampUtc: nowUtc);
|
|
}
|
|
return cache;
|
|
}
|
|
|
|
/// <summary>Disposes the evaluator and all cached script evaluators.</summary>
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
foreach (var ev in _cache.Values)
|
|
{
|
|
try { ev.Dispose(); } catch { /* best-effort */ }
|
|
}
|
|
_cache.Clear();
|
|
}
|
|
}
|