perf(site-runtime): per-attribute child routing in InstanceActor — attribute changes reach only interested children (P2)
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>How an attribute change should be routed to a child (script/alarm) actor.</summary>
|
||||
internal enum TriggerRoutingKind
|
||||
{
|
||||
/// <summary>Reacts only to changes of a single named attribute.</summary>
|
||||
MonitoredAttribute,
|
||||
|
||||
/// <summary>Reacts to ANY attribute change (Expression triggers; or fail-open when the
|
||||
/// trigger config is unparseable — the child's own gate then discards what it ignores).</summary>
|
||||
AllChanges,
|
||||
|
||||
/// <summary>Never reacts to attribute changes (interval / call-only scripts).</summary>
|
||||
None
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of classifying a child's trigger for attribute-change routing.
|
||||
/// </summary>
|
||||
internal readonly record struct TriggerRoutingDecision(TriggerRoutingKind Kind, string? MonitoredAttribute)
|
||||
{
|
||||
public static readonly TriggerRoutingDecision AllChanges = new(TriggerRoutingKind.AllChanges, null);
|
||||
public static readonly TriggerRoutingDecision None = new(TriggerRoutingKind.None, null);
|
||||
public static TriggerRoutingDecision Monitored(string attribute) => new(TriggerRoutingKind.MonitoredAttribute, attribute);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single source of truth for the monitored attribute a script/alarm trigger reacts to (P2).
|
||||
/// Shared by <see cref="ScriptActor"/> (attribute-name extraction) and
|
||||
/// <c>InstanceActor.CreateChildActors</c> (routing-map construction) so both agree on the
|
||||
/// attribute for a given trigger config.
|
||||
///
|
||||
/// NOTE: routing has three outcomes (monitored / all-changes / none), so this returns a
|
||||
/// <see cref="TriggerRoutingDecision"/> rather than the plan's original bool-out signature —
|
||||
/// a bool cannot distinguish "hears everything" from "hears nothing".
|
||||
/// </summary>
|
||||
internal static class TriggerRouting
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the monitored attribute name from a trigger config, honoring both the
|
||||
/// <c>"attributeName"</c> (scripts + alarms) and <c>"attribute"</c> (alarm alias) keys.
|
||||
/// Returns false when the JSON is missing, malformed, or carries no attribute name.
|
||||
/// </summary>
|
||||
public static bool TryReadAttributeName(string? triggerConfigJson, out string attribute)
|
||||
{
|
||||
attribute = "";
|
||||
if (string.IsNullOrEmpty(triggerConfigJson)) return false;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(triggerConfigJson);
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("attributeName", out var a) && a.GetString() is { Length: > 0 } n)
|
||||
{
|
||||
attribute = n;
|
||||
return true;
|
||||
}
|
||||
if (root.TryGetProperty("attribute", out var a2) && a2.GetString() is { Length: > 0 } n2)
|
||||
{
|
||||
attribute = n2;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies a script trigger. valuechange/conditional → the named attribute (fail open to
|
||||
/// all-changes if unparseable); expression → all-changes; interval/call → none; anything
|
||||
/// unknown → all-changes (fail open — a misconfigured script still reacts, its own gate
|
||||
/// remains the correctness check).
|
||||
/// </summary>
|
||||
public static TriggerRoutingDecision ForScript(string? triggerType, string? triggerConfigJson)
|
||||
{
|
||||
switch (triggerType?.ToLowerInvariant())
|
||||
{
|
||||
case "valuechange":
|
||||
case "conditional":
|
||||
return TryReadAttributeName(triggerConfigJson, out var attr)
|
||||
? TriggerRoutingDecision.Monitored(attr)
|
||||
: TriggerRoutingDecision.AllChanges;
|
||||
case "expression":
|
||||
return TriggerRoutingDecision.AllChanges;
|
||||
case "interval":
|
||||
case "call":
|
||||
return TriggerRoutingDecision.None;
|
||||
default:
|
||||
return TriggerRoutingDecision.AllChanges;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies an alarm trigger. Expression → all-changes; HiLo/ValueMatch/RangeViolation/
|
||||
/// RateOfChange → the named attribute (fail open to all-changes if unparseable); an
|
||||
/// unknown/unparseable trigger type → all-changes (fail open).
|
||||
/// </summary>
|
||||
public static TriggerRoutingDecision ForAlarm(string? triggerType, string? triggerConfigJson)
|
||||
{
|
||||
if (Enum.TryParse<AlarmTriggerType>(triggerType, ignoreCase: true, out var tt))
|
||||
{
|
||||
switch (tt)
|
||||
{
|
||||
case AlarmTriggerType.Expression:
|
||||
return TriggerRoutingDecision.AllChanges;
|
||||
case AlarmTriggerType.HiLo:
|
||||
case AlarmTriggerType.ValueMatch:
|
||||
case AlarmTriggerType.RangeViolation:
|
||||
case AlarmTriggerType.RateOfChange:
|
||||
return TryReadAttributeName(triggerConfigJson, out var attr)
|
||||
? TriggerRoutingDecision.Monitored(attr)
|
||||
: TriggerRoutingDecision.AllChanges;
|
||||
}
|
||||
}
|
||||
return TriggerRoutingDecision.AllChanges;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user