perf(site-runtime): deserialize a deploy's flattened config once and hand the parsed object down (review 2026-10-21 01/SR-P5)
The deploy compile warm parses once off-thread; the parsed FlattenedConfiguration rides DeployCompileWarmed to the gate (new Validate(FlattenedConfiguration) overload), the redeploy buffer, CreateInstanceActor's DCL ensure and a new InstanceActor constructor overload. Stored-row paths parse once inside CreateInstanceActor; an unparseable stored row still fails init via S6.
This commit is contained in:
@@ -11,6 +11,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.ScriptExecution;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Deployment;
|
||||
@@ -985,9 +986,16 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
var configJson = command.FlattenedConfigurationJson;
|
||||
Task.Run(() =>
|
||||
{
|
||||
// Review 2026-10-21 01/SR-P5: this is the ONE deserialization of the deploy's config.
|
||||
// The parsed object rides back on DeployCompileWarmed to the gate, the DCL-connection
|
||||
// ensure and the Instance Actor, which each used to parse the same JSON again — two
|
||||
// of those parses on this singleton's own thread.
|
||||
if (!DeployCompileValidator.TryParse(configJson, out var parsed, out var parseError))
|
||||
return new DeployCompileWarmed(command, null, parseError);
|
||||
|
||||
try
|
||||
{
|
||||
validator.Validate(configJson);
|
||||
validator.Validate(parsed!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -996,7 +1004,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
command.InstanceUniqueName);
|
||||
}
|
||||
|
||||
return new DeployCompileWarmed(command);
|
||||
return new DeployCompileWarmed(command, parsed, null);
|
||||
}).PipeTo(Self);
|
||||
}
|
||||
|
||||
@@ -1019,7 +1027,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
_deployWarms.Remove(instanceName);
|
||||
|
||||
RunDeployGateAndProceed(warming.Command, warming.ReplyTo);
|
||||
RunDeployGateAndProceed(warming.Command, warming.ReplyTo, msg.Parsed, msg.ParseError);
|
||||
|
||||
foreach (var queued in warming.Buffered)
|
||||
DispatchBufferedCommand(queued);
|
||||
@@ -1072,7 +1080,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// only from <see cref="HandleDeployCompileWarmed"/>, i.e. after the same compile has
|
||||
/// been warmed off-thread.
|
||||
/// </summary>
|
||||
private void RunDeployGateAndProceed(DeployInstanceCommand command, IActorRef replyTo)
|
||||
/// <param name="command">The deploy command being gated.</param>
|
||||
/// <param name="replyTo">The deployer to answer.</param>
|
||||
/// <param name="parsed">The config parsed once by the warm (01/SR-P5), or null on a parse failure.</param>
|
||||
/// <param name="parseError">The warm's deserialization error, when <paramref name="parsed"/> is null.</param>
|
||||
private void RunDeployGateAndProceed(
|
||||
DeployInstanceCommand command, IActorRef replyTo,
|
||||
FlattenedConfiguration? parsed, string? parseError)
|
||||
{
|
||||
// Site-side compile gate (S3): a compile failure must reject the
|
||||
// deployment with NO partial state applied (no Instance Actor, no
|
||||
@@ -1092,8 +1106,9 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// The gate also rejects any instance/script/alarm/native-source name that is not a legal
|
||||
// Akka actor name (review 2026-10-21 01/SR-C1) — such a name used to throw out of
|
||||
// Context.ActorOf on THIS actor's thread and restart the singleton with every instance.
|
||||
var errors = _deployCompileValidator.Validate(
|
||||
command.FlattenedConfigurationJson, command.InstanceUniqueName);
|
||||
var errors = parsed == null
|
||||
? [parseError ?? "Configuration JSON failed to deserialize."]
|
||||
: _deployCompileValidator.Validate(parsed, command.InstanceUniqueName);
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
var instanceName = command.InstanceUniqueName;
|
||||
@@ -1109,14 +1124,15 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
return;
|
||||
}
|
||||
|
||||
ProceedWithDeploy(command, replyTo);
|
||||
ProceedWithDeploy(command, replyTo, parsed!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core deploy application (redeploy-supersede + fresh apply), reached only
|
||||
/// after the synchronous compile gate in <see cref="HandleDeploy"/> passes.
|
||||
/// </summary>
|
||||
private void ProceedWithDeploy(DeployInstanceCommand command, IActorRef replyTo)
|
||||
private void ProceedWithDeploy(
|
||||
DeployInstanceCommand command, IActorRef replyTo, FlattenedConfiguration parsed)
|
||||
{
|
||||
var instanceName = command.InstanceUniqueName;
|
||||
_logger.LogInformation(
|
||||
@@ -1129,7 +1145,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
if (_instanceActors.TryGetValue(instanceName, out var existing))
|
||||
{
|
||||
_instanceActors.Remove(instanceName);
|
||||
_pendingRedeploys[existing] = new PendingRedeploy(command, replyTo);
|
||||
_pendingRedeploys[existing] = new PendingRedeploy(command, replyTo, parsed);
|
||||
_terminatingActorsByName[instanceName] = existing;
|
||||
Context.Watch(existing);
|
||||
_redeployStopQueue.Enqueue(existing);
|
||||
@@ -1157,12 +1173,12 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
$"superseded by newer deployment {command.DeploymentId} before predecessor finished terminating",
|
||||
DateTimeOffset.UtcNow));
|
||||
}
|
||||
_pendingRedeploys[terminatingRef] = new PendingRedeploy(command, replyTo);
|
||||
_pendingRedeploys[terminatingRef] = new PendingRedeploy(command, replyTo, parsed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fresh deployment — no existing actor to replace.
|
||||
ApplyDeployment(command, replyTo, isRedeploy: false);
|
||||
ApplyDeployment(command, replyTo, isRedeploy: false, parsed);
|
||||
}
|
||||
|
||||
private bool DeferLifecycleStop(string name, string commandId, bool delete, IActorRef replyTo)
|
||||
@@ -1224,7 +1240,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
_terminatingActorsByName.Remove(pending.Command.InstanceUniqueName);
|
||||
if (flush.Error is null)
|
||||
ApplyDeployment(pending.Command, pending.OriginalSender, isRedeploy: true);
|
||||
ApplyDeployment(pending.Command, pending.OriginalSender, isRedeploy: true, pending.Parsed);
|
||||
else
|
||||
pending.OriginalSender.Tell(new DeploymentStatusResponse(pending.Command.DeploymentId,
|
||||
pending.Command.InstanceUniqueName, DeploymentStatus.Failed,
|
||||
@@ -1381,7 +1397,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// incorrect. The reply is sent from <see cref="HandleDeployPersistenceResult"/> once
|
||||
/// the persistence outcome is known.
|
||||
/// </summary>
|
||||
private void ApplyDeployment(DeployInstanceCommand command, IActorRef sender, bool isRedeploy)
|
||||
private void ApplyDeployment(
|
||||
DeployInstanceCommand command, IActorRef sender, bool isRedeploy, FlattenedConfiguration? parsed)
|
||||
{
|
||||
var instanceName = command.InstanceUniqueName;
|
||||
|
||||
@@ -1413,7 +1430,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// connections this config's data-sourced attributes subscribe through (register row 85).
|
||||
if (!CreateInstanceActor(
|
||||
instanceName, command.FlattenedConfigurationJson, command.RevisionHash,
|
||||
out var createError, staticStateResetGate))
|
||||
out var createError, staticStateResetGate, parsed))
|
||||
{
|
||||
// Unreachable while the name pre-check above holds; kept so an ActorOf refusal can
|
||||
// never escape to the supervisor and restart the singleton.
|
||||
@@ -2888,10 +2905,32 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
if (_dclManager == null) return;
|
||||
|
||||
FlattenedConfiguration? config;
|
||||
try
|
||||
{
|
||||
var config = System.Text.Json.JsonSerializer.Deserialize<Commons.Types.Flattening.FlattenedConfiguration>(configJson);
|
||||
if (config?.Connections == null) return;
|
||||
config = System.Text.Json.JsonSerializer.Deserialize<FlattenedConfiguration>(configJson);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to parse flattened config for DCL connections");
|
||||
return;
|
||||
}
|
||||
|
||||
if (config != null)
|
||||
EnsureDclConnections(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="EnsureDclConnections(string)"/> over an already-parsed configuration
|
||||
/// (review 2026-10-21 01/SR-P5) — the form every creation path now uses.
|
||||
/// </summary>
|
||||
private void EnsureDclConnections(FlattenedConfiguration config)
|
||||
{
|
||||
if (_dclManager == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (config.Connections == null) return;
|
||||
|
||||
foreach (var (name, connConfig) in config.Connections)
|
||||
{
|
||||
@@ -2928,7 +2967,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to parse flattened config for DCL connections");
|
||||
_logger.LogWarning(ex, "Failed to apply flattened config DCL connections");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3745,15 +3784,27 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// <see langword="null"/> on every non-deploy start (boot batch, enable, self-heal), where
|
||||
/// nothing is being reset and the load runs immediately as before.
|
||||
/// </param>
|
||||
/// <param name="parsed">
|
||||
/// <paramref name="configJson"/> already deserialized by the caller (the deploy warm or the
|
||||
/// boot-batch warm — review 2026-10-21 01/SR-P5), or <see langword="null"/> to parse it here,
|
||||
/// ONCE, for both the DCL-connection ensure and the Instance Actor.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="false"/> when Akka refused the actor name (review 2026-10-21 01/SR-C1);
|
||||
/// nothing was created or recorded. Callers turn that into a Failed reply / site event.
|
||||
/// </returns>
|
||||
internal bool CreateInstanceActor(
|
||||
string instanceName, string configJson, string revisionHash, out string? error,
|
||||
Task? staticStateResetGate = null)
|
||||
Task? staticStateResetGate = null, FlattenedConfiguration? parsed = null)
|
||||
{
|
||||
error = null;
|
||||
|
||||
// 01/SR-P5: one parse for the DCL ensure AND the actor. A config that does not parse is
|
||||
// NOT handled here: the JSON is handed to the Instance Actor unchanged, whose constructor
|
||||
// throws exactly as before (ActorInitializationException → Stop → the S6 path), so the
|
||||
// failure semantics of a poison stored config are unchanged.
|
||||
if (parsed == null && !DeployCompileValidator.TryParse(configJson, out parsed, out _))
|
||||
parsed = null;
|
||||
// Deferred register row 85 — an Instance Actor is useless without the DCL connection
|
||||
// actors its data-sourced attributes subscribe through, so ensuring them belongs in the
|
||||
// single creation funnel rather than at each caller.
|
||||
@@ -3777,7 +3828,10 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// their exact previous semantics (they called it unconditionally). It is cheap and
|
||||
// silent when there is nothing to do: EnsureDclConnection is hash-guarded on
|
||||
// _createdConnections, so an unchanged connection is a no-op with no message and no log.
|
||||
EnsureDclConnections(configJson);
|
||||
if (parsed != null)
|
||||
EnsureDclConnections(parsed);
|
||||
else
|
||||
EnsureDclConnections(configJson);
|
||||
|
||||
if (_instanceActors.ContainsKey(instanceName))
|
||||
{
|
||||
@@ -3787,20 +3841,36 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
// Reuse the shared, host-configured logger factory
|
||||
// instead of allocating (and leaking) a fresh LoggerFactory per instance.
|
||||
var props = Props.Create(() => new InstanceActor(
|
||||
instanceName,
|
||||
configJson,
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
_streamManager,
|
||||
_options,
|
||||
_loggerFactory.CreateLogger<InstanceActor>(),
|
||||
_dclManager,
|
||||
_healthCollector,
|
||||
_serviceProvider,
|
||||
null,
|
||||
staticStateResetGate));
|
||||
var instanceLogger = _loggerFactory.CreateLogger<InstanceActor>();
|
||||
var props = parsed != null
|
||||
? Props.Create(() => new InstanceActor(
|
||||
instanceName,
|
||||
parsed,
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
_streamManager,
|
||||
_options,
|
||||
instanceLogger,
|
||||
_dclManager,
|
||||
_healthCollector,
|
||||
_serviceProvider,
|
||||
null,
|
||||
staticStateResetGate))
|
||||
: Props.Create(() => new InstanceActor(
|
||||
instanceName,
|
||||
configJson,
|
||||
_storage,
|
||||
_compilationService,
|
||||
_sharedScriptLibrary,
|
||||
_streamManager,
|
||||
_options,
|
||||
instanceLogger,
|
||||
_dclManager,
|
||||
_healthCollector,
|
||||
_serviceProvider,
|
||||
null,
|
||||
staticStateResetGate));
|
||||
|
||||
// Review 2026-10-21 01/SR-C1: ActorOf throws InvalidActorNameException SYNCHRONOUSLY on
|
||||
// this actor's thread for a name Akka refuses (space, '#', '/', non-ASCII, ...). Escaping
|
||||
@@ -3978,7 +4048,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// command instance it warmed so a result superseded mid-warm is recognised by reference
|
||||
/// and dropped.
|
||||
/// </summary>
|
||||
internal sealed record DeployCompileWarmed(DeployInstanceCommand Command);
|
||||
/// <param name="Command">The exact command instance the warm ran for.</param>
|
||||
/// <param name="Parsed">The config parsed once off-thread (01/SR-P5); null when it did not parse.</param>
|
||||
/// <param name="ParseError">Why it did not parse, when <paramref name="Parsed"/> is null.</param>
|
||||
internal sealed record DeployCompileWarmed(
|
||||
DeployInstanceCommand Command, FlattenedConfiguration? Parsed = null, string? ParseError = null);
|
||||
|
||||
/// <summary>
|
||||
/// Piped back to self once a staggered startup batch's compiles have been
|
||||
@@ -4100,7 +4174,11 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// <summary>
|
||||
/// A redeployment command buffered until the previous Instance Actor terminates.
|
||||
/// </summary>
|
||||
internal record PendingRedeploy(DeployInstanceCommand Command, IActorRef OriginalSender);
|
||||
/// <param name="Command">The buffered deploy.</param>
|
||||
/// <param name="OriginalSender">The deployer to answer.</param>
|
||||
/// <param name="Parsed">The config the gate already parsed (01/SR-P5), reused on apply.</param>
|
||||
internal record PendingRedeploy(
|
||||
DeployInstanceCommand Command, IActorRef OriginalSender, FlattenedConfiguration? Parsed = null);
|
||||
|
||||
/// <summary>
|
||||
/// Join state for a deployment awaiting confirmation. Deployment <c>Success</c> is
|
||||
|
||||
Reference in New Issue
Block a user