fix(site-runtime): reject invalid runtime actor names at the site gate and never let ActorOf escalate to the Deployment Manager (review 2026-10-21 01/SR-C1)
An instance unique name Akka refuses as a path element (space, '#', '/', non-ASCII) made Context.ActorOf throw on the singleton's own thread; the default Restart stopped every Instance Actor on the site. DeployCompileValidator now checks the instance + script/alarm/native-source actor names with ActorPath.IsValidPathElement (the Commons ActorNameRules mirror is stricter than Akka for " ( ), pinned by a parity sweep), CreateInstanceActor catches InvalidActorNameException and every caller reports it, and the Instance Actor skips a refused child name like a compile failure.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using Akka.Actor;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
@@ -30,6 +31,18 @@ public sealed class DeployCompileValidator
|
||||
public DeployCompileValidator(ScriptCompilationService compilationService)
|
||||
=> _compilationService = compilationService;
|
||||
|
||||
/// <summary>
|
||||
/// Prefix carried by every runtime-name error, so a caller can tell a name rejection
|
||||
/// (<see cref="IsActorNameError"/>) from a compile failure when wording its reply.
|
||||
/// </summary>
|
||||
private const string ActorNameErrorPrefix = "Invalid runtime name: ";
|
||||
|
||||
/// <summary>True when <paramref name="error"/> came from the runtime-name check.</summary>
|
||||
/// <param name="error">One error returned by <see cref="Validate"/>.</param>
|
||||
/// <returns><see langword="true"/> for a runtime-name error.</returns>
|
||||
public static bool IsActorNameError(string error) =>
|
||||
error.StartsWith(ActorNameErrorPrefix, StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Validates every compilable artifact in the flattened configuration.
|
||||
/// Returns an empty list when the whole configuration compiles; otherwise
|
||||
@@ -37,8 +50,13 @@ public sealed class DeployCompileValidator
|
||||
/// single error (this also fronts the poison-config case for startup load).
|
||||
/// </summary>
|
||||
/// <param name="configJson">The flattened-configuration JSON to validate.</param>
|
||||
/// <param name="instanceName">
|
||||
/// The instance unique name the Deployment Manager will create the Instance Actor under.
|
||||
/// When supplied it is checked as an actor name along with every script/alarm/native-source
|
||||
/// child name (review 2026-10-21 01/SR-C1); <see langword="null"/> checks the child names only.
|
||||
/// </param>
|
||||
/// <returns>A list of compile-error messages; empty if the configuration is clean.</returns>
|
||||
public IReadOnlyList<string> Validate(string configJson)
|
||||
public IReadOnlyList<string> Validate(string configJson, string? instanceName = null)
|
||||
{
|
||||
FlattenedConfiguration? config;
|
||||
try
|
||||
@@ -55,6 +73,10 @@ public sealed class DeployCompileValidator
|
||||
|
||||
var errors = new List<string>();
|
||||
|
||||
// Runtime names first: they are cheap, and a name Akka refuses would otherwise surface
|
||||
// as an InvalidActorNameException at Context.ActorOf instead of a readable rejection.
|
||||
AddActorNameErrors(errors, config, instanceName);
|
||||
|
||||
// Scripts: full execution-path compile against ScriptGlobals.
|
||||
foreach (var script in config.Scripts)
|
||||
{
|
||||
@@ -97,6 +119,48 @@ public sealed class DeployCompileValidator
|
||||
return errors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 01/SR-C1: every name that becomes an Akka actor name on the site — the
|
||||
/// Instance Actor itself (<c>DeploymentManagerActor.CreateInstanceActor</c>) and its
|
||||
/// <c>script-</c>/<c>alarm-</c>/<c>native-alarm-</c> children
|
||||
/// (<c>InstanceActor.CreateChildActors</c>) — must be a legal path element. An instance named
|
||||
/// <c>"Furnace #3"</c> used to make <c>Context.ActorOf</c> throw on the Deployment Manager
|
||||
/// singleton's own thread, restarting it and every Instance Actor on the site.
|
||||
///
|
||||
/// <para>
|
||||
/// Decided with Akka's own <see cref="ActorPath.IsValidPathElement"/>, not the Commons mirror
|
||||
/// <c>ActorNameRules</c>: the mirror is currently STRICTER (it refuses <c>"</c>, <c>(</c> and
|
||||
/// <c>)</c>, which Akka accepts), so gating on it would reject deployments that work today.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static void AddActorNameErrors(
|
||||
List<string> errors, FlattenedConfiguration config, string? instanceName)
|
||||
{
|
||||
if (instanceName != null)
|
||||
AddActorNameError(errors, "Instance", instanceName, instanceName);
|
||||
|
||||
foreach (var script in config.Scripts)
|
||||
AddActorNameError(errors, "Script", script.CanonicalName, $"script-{script.CanonicalName}");
|
||||
|
||||
foreach (var alarm in config.Alarms)
|
||||
AddActorNameError(errors, "Alarm", alarm.CanonicalName, $"alarm-{alarm.CanonicalName}");
|
||||
|
||||
foreach (var source in config.NativeAlarmSources)
|
||||
AddActorNameError(errors, "Native alarm source", source.CanonicalName,
|
||||
$"native-alarm-{source.CanonicalName}");
|
||||
}
|
||||
|
||||
private static void AddActorNameError(
|
||||
List<string> errors, string kind, string? name, string actorName)
|
||||
{
|
||||
if (ActorPath.IsValidPathElement(actorName))
|
||||
return;
|
||||
|
||||
errors.Add($"{ActorNameErrorPrefix}{kind} name '{name}' cannot be used as a runtime actor " +
|
||||
"name — use ASCII letters, digits and -_.*$+:@&=,!~';() only (no spaces, '#', " +
|
||||
"'/', '[', ']', '%' or non-ASCII letters), and do not start it with '$'.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the boolean trigger expression for an Expression-triggered script
|
||||
/// or alarm and records any failure. No-op for non-Expression triggers or a
|
||||
|
||||
Reference in New Issue
Block a user