docs(comments): strip internal task/milestone/bundle bookkeeping from code comments

Remove project bookkeeping citations from shipped code comments across the
solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/
issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels,
and C/D/K/S/T phase labels.

Comment text only — no code logic, string/log literals, or XML-doc structure
changed. Genuine descriptions are preserved (only the citation is stripped),
and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365,
UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker
TaskReferenceInComment / TrackingReferenceInComment checks plus targeted
grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
Joseph Doherty
2026-07-07 11:03:26 -04:00
parent 67005ca4c0
commit 9cff87fe85
435 changed files with 2338 additions and 2547 deletions
@@ -14,7 +14,7 @@ using System.Text.Json;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
/// <summary>
/// WP-16: Alarm Actor — coordinator actor, child of Instance Actor, peer to Script Actors.
/// Alarm Actor — coordinator actor, child of Instance Actor, peer to Script Actors.
/// Subscribes to attribute change notifications from Instance Actor.
///
/// Evaluates alarm conditions:
@@ -25,7 +25,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
/// State (active/normal) is in memory only, NOT persisted.
/// On restart: starts normal, re-evaluates from incoming values.
///
/// WP-21: AlarmExecutionActor CAN call Instance.CallScript() (ask to sibling Script Actor).
/// AlarmExecutionActor CAN call Instance.CallScript() (ask to sibling Script Actor).
/// Instance scripts CANNOT call alarm on-trigger scripts (no Instance.CallAlarmScript API).
///
/// Supervision: Resume on exception; AlarmExecutionActor stopped on exception.
@@ -42,7 +42,7 @@ public class AlarmActor : ReceiveActor
private readonly IServiceProvider? _serviceProvider;
/// <summary>
/// M1.5: the optional site operational-event log, resolved once from
/// The optional site operational-event log, resolved once from
/// <see cref="_serviceProvider"/> at construction and cached. The
/// registration is process-lifetime (a singleton), so resolving once on
/// the actor's own thread is both correct and cheaper than a per-event
@@ -53,7 +53,7 @@ public class AlarmActor : ReceiveActor
private readonly ISiteEventLogger? _siteEventLogger;
/// <summary>
/// M1.5: priority at or above which a computed-alarm raise is logged as
/// Priority at or above which a computed-alarm raise is logged as
/// <c>Error</c> to the site event log; below it, raises log as <c>Warning</c>.
/// Mirrors the 01000 alarm-severity scale.
/// </summary>
@@ -73,7 +73,7 @@ public class AlarmActor : ReceiveActor
private readonly Script<object?>? _onTriggerCompiledScript;
/// <summary>
/// M2.5 (#9): the on-trigger script's per-script execution timeout in seconds,
/// The on-trigger script's per-script execution timeout in seconds,
/// or null to use the global default. Forwarded to each spawned
/// <see cref="AlarmExecutionActor"/>, which applies <c>perScript ?? global</c>
/// (treating ≤ 0 as "use global"). The value comes from the referenced
@@ -88,7 +88,7 @@ public class AlarmActor : ReceiveActor
private readonly Dictionary<string, object?> _attributeSnapshot = new();
/// <summary>
/// SiteRuntime-017: the exact dictionary instance this actor was seeded from
/// The exact dictionary instance this actor was seeded from
/// at construction. The Instance Actor must pass a private snapshot here, not
/// its live <c>_attributes</c> field. Exposed for regression coverage of that
/// isolation contract.
@@ -114,9 +114,9 @@ public class AlarmActor : ReceiveActor
/// <param name="initialAttributes">Seed attribute snapshot so static attributes evaluate correctly at startup.</param>
/// <param name="healthCollector">Optional health collector for surfacing alarm execution metrics.</param>
/// <param name="serviceProvider">Optional DI service provider used to resolve the optional
/// <see cref="ISiteEventLogger"/> for M1.5 <c>alarm</c> operational events. Fire-and-forget;
/// <see cref="ISiteEventLogger"/> for <c>alarm</c> operational events. Fire-and-forget;
/// a logging failure never affects alarm evaluation.</param>
/// <param name="onTriggerExecutionTimeoutSeconds">M2.5 (#9): the on-trigger script's per-script
/// <param name="onTriggerExecutionTimeoutSeconds">The on-trigger script's per-script
/// execution timeout in seconds (from its <see cref="ResolvedScript.ExecutionTimeoutSeconds"/>),
/// or null/non-positive to use the global default.</param>
public AlarmActor(
@@ -132,7 +132,7 @@ public class AlarmActor : ReceiveActor
IReadOnlyDictionary<string, object?>? initialAttributes = null,
ISiteHealthCollector? healthCollector = null,
IServiceProvider? serviceProvider = null,
// M2.5 (#9): per-script timeout for the on-trigger script (null = global).
// Per-script timeout for the on-trigger script (null = global).
int? onTriggerExecutionTimeoutSeconds = null)
{
_alarmName = alarmName;
@@ -143,7 +143,7 @@ public class AlarmActor : ReceiveActor
_logger = logger;
_healthCollector = healthCollector;
_serviceProvider = serviceProvider;
// M1.5: resolve the optional site event logger once and cache it,
// Resolve the optional site event logger once and cache it,
// rather than calling GetService on every alarm transition.
_siteEventLogger = serviceProvider?.GetService<ISiteEventLogger>();
_priority = alarmConfig.PriorityLevel;
@@ -252,7 +252,7 @@ public class AlarmActor : ReceiveActor
_instanceName, _alarmName, AlarmState.Active, _priority, DateTimeOffset.UtcNow);
_instanceActor.Tell(alarmChanged);
// M1.5: operational `alarm` event — raise. Severity by priority.
// Operational `alarm` event — raise. Severity by priority.
LogAlarmEvent(RaiseSeverity(_priority), $"Alarm {_alarmName} activated (priority {_priority})");
// Spawn AlarmExecutionActor if on-trigger script defined
@@ -273,7 +273,7 @@ public class AlarmActor : ReceiveActor
_instanceName, _alarmName, AlarmState.Normal, _priority, DateTimeOffset.UtcNow);
_instanceActor.Tell(alarmChanged);
// M1.5: operational `alarm` event — return to normal.
// Operational `alarm` event — return to normal.
LogAlarmEvent("Info", $"Alarm {_alarmName} cleared");
}
}
@@ -315,7 +315,7 @@ public class AlarmActor : ReceiveActor
};
_instanceActor.Tell(alarmChanged);
// M1.5: operational `alarm` event. Entering a band from Normal is a raise
// Operational `alarm` event. Entering a band from Normal is a raise
// (severity by the band's priority); returning to None is a clear; a
// level-to-level escalation/de-escalation is an informational transition.
if (newLevel == AlarmLevel.None)
@@ -342,7 +342,7 @@ public class AlarmActor : ReceiveActor
}
/// <summary>
/// M1.5: maps an alarm priority (01000) to a site-event severity for a
/// Maps an alarm priority (01000) to a site-event severity for a
/// <i>raise</i> transition — <c>Error</c> at or above
/// <see cref="ErrorPriorityThreshold"/>, otherwise <c>Warning</c>. Clears and
/// inter-band transitions always log as <c>Info</c>.
@@ -351,7 +351,7 @@ public class AlarmActor : ReceiveActor
priority >= ErrorPriorityThreshold ? "Error" : "Warning";
/// <summary>
/// M1.5: fire-and-forget an <c>alarm</c> operational event to the optional
/// Fire-and-forget an <c>alarm</c> operational event to the optional
/// <see cref="ISiteEventLogger"/> (resolved once at construction and cached
/// in <see cref="_siteEventLogger"/>). Never awaited so a logging failure
/// cannot affect alarm evaluation (matching the established
@@ -429,7 +429,7 @@ public class AlarmActor : ReceiveActor
try
{
// InvariantCulture so string attribute values parse consistently
// regardless of host locale (SiteRuntime-023).
// regardless of host locale.
var numericValue = Convert.ToDouble(value, CultureInfo.InvariantCulture);
return numericValue < config.Min || numericValue > config.Max;
}
@@ -447,7 +447,7 @@ public class AlarmActor : ReceiveActor
try
{
// InvariantCulture so string attribute values parse consistently
// regardless of host locale (SiteRuntime-023).
// regardless of host locale.
var numericValue = Convert.ToDouble(value, CultureInfo.InvariantCulture);
// Add to window
@@ -537,7 +537,7 @@ public class AlarmActor : ReceiveActor
double numericValue;
// InvariantCulture so string attribute values parse consistently
// regardless of host locale (SiteRuntime-023).
// regardless of host locale.
try { numericValue = Convert.ToDouble(value, CultureInfo.InvariantCulture); }
catch { return _currentLevel; }
@@ -575,7 +575,7 @@ public class AlarmActor : ReceiveActor
/// <param name="priority">The firing alarm priority.</param>
/// <param name="message">The firing alarm message.</param>
/// <param name="parentExecutionId">
/// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): the execution id of
/// The execution id of
/// the context that fired this alarm, recorded as the on-trigger script run's
/// <c>ParentExecutionId</c> so the alarm-triggered run chains under its firing
/// context in the audit tree. The alarm subsystem currently has no Guid-typed
@@ -590,7 +590,7 @@ public class AlarmActor : ReceiveActor
var executionId = $"{_alarmName}-alarm-exec-{_executionCounter++}";
// SiteRuntime-009: the on-trigger script body runs on the dedicated
// The on-trigger script body runs on the dedicated
// ScriptExecutionScheduler, not the shared .NET thread pool.
var props = Props.Create(() => new AlarmExecutionActor(
_alarmName,
@@ -603,9 +603,9 @@ public class AlarmActor : ReceiveActor
_sharedScriptLibrary,
_options,
_logger,
// M2.5 (#9): per-script timeout from the on-trigger script (null = global).
// Per-script timeout from the on-trigger script (null = global).
_onTriggerExecutionTimeoutSeconds,
// Audit Log #23 (M5.4): the firing context's execution id (null today).
// The firing context's execution id (null today).
parentExecutionId));
Context.ActorOf(props, executionId);
@@ -9,9 +9,9 @@ using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
/// <summary>
/// WP-16: Alarm Execution Actor -- short-lived child of Alarm Actor.
/// Alarm Execution Actor -- short-lived child of Alarm Actor.
/// Same pattern as ScriptExecutionActor.
/// WP-21: CAN call Instance.CallScript() (ask to sibling Script Actor).
/// CAN call Instance.CallScript() (ask to sibling Script Actor).
/// Instance scripts CANNOT call alarm on-trigger scripts (no API for it).
/// Supervision: Stop on unhandled exception.
/// </summary>
@@ -28,9 +28,9 @@ public class AlarmExecutionActor : ReceiveActor
/// <param name="sharedScriptLibrary">Shared script library providing common utilities.</param>
/// <param name="options">Site runtime configuration options, including the execution timeout.</param>
/// <param name="logger">Logger for execution diagnostics.</param>
/// <param name="executionTimeoutSeconds">M2.5 (#9): the on-trigger script's per-script execution timeout in seconds. Null or non-positive falls back to the global <see cref="SiteRuntimeOptions.ScriptExecutionTimeoutSeconds"/>.</param>
/// <param name="executionTimeoutSeconds">The on-trigger script's per-script execution timeout in seconds. Null or non-positive falls back to the global <see cref="SiteRuntimeOptions.ScriptExecutionTimeoutSeconds"/>.</param>
/// <param name="parentExecutionId">
/// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): the execution id of
/// ParentExecutionId tag-cascade: the execution id of
/// the context that fired this alarm, threaded into the on-trigger script's
/// <see cref="ScriptRuntimeContext"/> as its <c>ParentExecutionId</c> so the
/// alarm-triggered run chains under its firing context. Null today (no
@@ -48,10 +48,10 @@ public class AlarmExecutionActor : ReceiveActor
SharedScriptLibrary sharedScriptLibrary,
SiteRuntimeOptions options,
ILogger logger,
// M2.5 (#9): per-script execution timeout override (seconds) for the
// Per-script execution timeout override (seconds) for the
// alarm on-trigger script. Null or non-positive falls back to the global.
int? executionTimeoutSeconds = null,
// Audit Log #23 (M5.4): the firing context's execution id (null today).
// The firing context's execution id (null today).
Guid? parentExecutionId = null)
{
var self = Self;
@@ -80,14 +80,14 @@ public class AlarmExecutionActor : ReceiveActor
int? executionTimeoutSeconds,
Guid? parentExecutionId)
{
// M2.5 (#9): per-script timeout overrides the global default. A null or
// Per-script timeout overrides the global default. A null or
// non-positive per-script value (≤ 0) falls back to the global.
var timeout = TimeSpan.FromSeconds(
executionTimeoutSeconds is { } perScript && perScript > 0
? perScript
: options.ScriptExecutionTimeoutSeconds);
// SiteRuntime-009: run the alarm on-trigger body on the dedicated
// Run the alarm on-trigger body on the dedicated
// script-execution scheduler, not the shared .NET thread pool.
var scheduler = ScriptExecutionScheduler.Shared(options);
@@ -96,7 +96,7 @@ public class AlarmExecutionActor : ReceiveActor
using var cts = new CancellationTokenSource(timeout);
try
{
// WP-21: AlarmExecutionActor can call Instance.CallScript()
// AlarmExecutionActor can call Instance.CallScript()
// via the ScriptRuntimeContext injected into globals
var context = new ScriptRuntimeContext(
instanceActor,
@@ -107,7 +107,7 @@ public class AlarmExecutionActor : ReceiveActor
timeout,
instanceName,
logger,
// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): the
// ParentExecutionId tag-cascade: the
// alarm on-trigger run mints its own fresh ExecutionId (the
// ctor's `?? NewGuid()` fallback) and records the firing
// context's id as its ParentExecutionId — null today, so the
@@ -147,7 +147,7 @@ public class AlarmExecutionActor : ReceiveActor
}
catch (Exception ex)
{
// WP-32: Failures logged, alarm continues
// Failures logged, alarm continues
logger.LogError(ex,
"Alarm on-trigger script for {Alarm} on {Instance} failed",
alarmName, instanceName);
@@ -7,13 +7,13 @@ using ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
/// <summary>
/// Per-node OPC UA certificate-store actor (T17 / D6). Runs on EVERY site
/// Per-node OPC UA certificate-store actor. Runs on EVERY site
/// node (NOT a singleton) at a well-known name so the Deployment Manager
/// singleton can address it on each node via <see cref="ActorSelection"/>.
///
/// Cert trust is site-local: the trusted-peer PKI store is a directory on each
/// node's file system. A trust/remove decision must therefore reach BOTH site
/// nodes or the two stores diverge across failover (D6). The singleton fans the
/// nodes or the two stores diverge across failover. The singleton fans the
/// per-node <see cref="WriteCertToLocalStore"/> / <see cref="RemoveCertFromLocalStore"/>
/// message out; this actor performs the actual file write/delete/enumerate.
///
@@ -40,8 +40,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
private readonly SiteRuntimeOptions _options;
private readonly ILogger<DeploymentManagerActor> _logger;
/// <summary>
/// Shared logger factory used to mint <see cref="InstanceActor"/> loggers
/// (SiteRuntime-015). Reused across every <see cref="CreateInstanceActor"/>
/// Shared logger factory used to mint <see cref="InstanceActor"/> loggers.
/// Reused across every <see cref="CreateInstanceActor"/>
/// call rather than newing a per-instance factory that is never disposed.
/// When the host injects its configured factory the Instance Actor logs are
/// routed through the application's logging providers.
@@ -52,7 +52,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
private readonly ISiteHealthCollector? _healthCollector;
private readonly IServiceProvider? _serviceProvider;
/// <summary>
/// Notify-and-fetch (Task 10): fetches a deployment's flattened config from central
/// Notify-and-fetch: fetches a deployment's flattened config from central
/// over HTTP when a <see cref="RefreshDeploymentCommand"/> arrives. Optional — null on
/// nodes/tests that never receive a refresh; the active site path supplies it via DI.
/// </summary>
@@ -61,12 +61,12 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// <summary>
/// Tracks Instance Actors that are terminating as part of a redeployment, keyed by
/// the terminating actor ref. The buffered command is applied once <see cref="Terminated"/>
/// confirms the child has fully stopped (SiteRuntime-003).
/// confirms the child has fully stopped.
/// </summary>
private readonly Dictionary<IActorRef, PendingRedeploy> _pendingRedeploys = new();
/// <summary>
/// SiteRuntime-020: name → terminating actor ref shadow of <see cref="_pendingRedeploys"/>.
/// Name → terminating actor ref shadow of <see cref="_pendingRedeploys"/>.
/// Required because a third <see cref="DeployInstanceCommand"/> for the same instance
/// arriving WHILE a redeploy is still mid-termination would otherwise see
/// <c>_instanceActors.TryGetValue == false</c> and fall through to
@@ -81,7 +81,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// </summary>
private readonly Dictionary<string, IActorRef> _terminatingActorsByName = new();
/// <summary>
/// SiteRuntime-032: authoritative set of deployed instance config names (enabled
/// Authoritative set of deployed instance config names (enabled
/// AND disabled). The deployed/disabled health counts are derived from this set's
/// size, so add-on-deploy / remove-on-delete keeps the count correct for every
/// path — including deleting a DISABLED instance, which has a config row but is
@@ -105,7 +105,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// <param name="serviceProvider">Optional service provider for resolving per-instance services.</param>
/// <param name="loggerFactory">Optional logger factory for creating Instance Actor loggers.</param>
/// <param name="configFetcher">
/// Optional notify-and-fetch config fetcher (Task 10). Required for the
/// Optional notify-and-fetch config fetcher. Required for the
/// <see cref="RefreshDeploymentCommand"/> path; null on nodes/tests that never
/// receive a refresh.
/// </param>
@@ -134,7 +134,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
_serviceProvider = serviceProvider;
_configFetcher = configFetcher;
_logger = logger;
// SiteRuntime-015: reuse a single logger factory for all Instance Actors.
// Reuse a single logger factory for all Instance Actors.
// Prefer an explicitly injected factory, fall back to one resolved from
// the service provider, and only as a last resort use NullLoggerFactory —
// never a per-instance `new LoggerFactory()` that leaks undisposed.
@@ -148,7 +148,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
Receive<EnableInstanceCommand>(HandleEnable);
Receive<DeleteInstanceCommand>(HandleDelete);
// Notify-and-fetch (Task 10): central sends a small RefreshDeploymentCommand;
// Notify-and-fetch: central sends a small RefreshDeploymentCommand;
// the active singleton fetches the flattened config over HTTP, then reuses the
// existing apply path. The two internal results carry the fetched config (or the
// fetch error) back onto the actor thread along with the captured original sender.
@@ -156,16 +156,16 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
Receive<RefreshFetched>(HandleRefreshFetched);
Receive<RefreshFetchFailed>(HandleRefreshFetchFailed);
// DeploymentManager-006: query-the-site-before-redeploy idempotency.
// Query-the-site-before-redeploy idempotency.
// Central asks for the instance's currently-applied deployment identity
// before re-sending a deployment whose prior record is stuck InProgress
// or Failed due to a timeout.
Receive<DeploymentStateQueryRequest>(HandleDeploymentStateQuery);
// WP-33: Handle system-wide artifact deployment
// Handle system-wide artifact deployment
Receive<DeployArtifactsCommand>(HandleDeployArtifacts);
// SiteRuntime-021: artifact-deploy DCL push, dispatched back from the
// Artifact-deploy DCL push, dispatched back from the
// off-thread persistence task so the hash-cache mutation stays
// actor-thread-confined.
Receive<ApplyArtifactDataConnectionsToDcl>(HandleApplyArtifactDataConnectionsToDcl);
@@ -197,8 +197,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
Receive<ReadTagValuesCommand>(msg =>
Context.ActorSelection("/user/dcl-manager").Tell(msg, Sender));
// OPC UA tag-picker address-space search (T15), secured-write execute (T14),
// and endpoint Verify (T17) — same singleton-only re-forward as the browse
// OPC UA tag-picker address-space search, secured-write execute,
// and endpoint Verify — same singleton-only re-forward as the browse
// handler above. SiteCommunicationActor routes these to this singleton
// (active node) so the local dcl-manager is the one holding the live
// DataConnectionActor children (Search/Write route there by connection name;
@@ -211,7 +211,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
Receive<VerifyEndpointCommand>(msg =>
Context.ActorSelection("/user/dcl-manager").Tell(msg, Sender));
// T17 / D6 — OPC UA server-certificate trust. Trust is site-local: the
// OPC UA server-certificate trust. Trust is site-local: the
// trusted-peer PKI store is per-node, so a trust/remove MUST reach BOTH
// site nodes (node-a + node-b) or they diverge across failover. This
// singleton fans the corresponding per-node message out to the
@@ -232,7 +232,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
Receive<DeployPersistenceResult>(HandleDeployPersistenceResult);
// Terminated signal — drains a buffered redeployment once the previous
// Instance Actor has fully stopped (SiteRuntime-003).
// Instance Actor has fully stopped.
Receive<Terminated>(HandleTerminated);
}
@@ -281,7 +281,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// <summary>
/// Processes the loaded configs from SQLite.
///
/// SiteRuntime-008: shared scripts must be compiled before Instance Actors are
/// Shared scripts must be compiled before Instance Actors are
/// created, but the SQLite read and Roslyn compilation must not block the
/// singleton's mailbox. The compilation is run on a background task and a
/// <see cref="SharedScriptsLoaded"/> message is piped back; only then does
@@ -310,7 +310,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// SiteRuntime-008: once shared scripts have been compiled off-thread, begins
/// Once shared scripts have been compiled off-thread, begins
/// staggered Instance Actor creation for the enabled configs captured at startup.
/// </summary>
private void HandleSharedScriptsLoaded(SharedScriptsLoaded msg)
@@ -407,7 +407,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
return;
}
// SiteRuntime-020: a deploy arriving while the previous redeploy is still
// A deploy arriving while the previous redeploy is still
// terminating (the Terminated signal hasn't fired yet) used to fall through
// to ApplyDeployment(fresh), where Context.ActorOf would throw
// InvalidActorNameException because the child name is still registered.
@@ -443,7 +443,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
if (!_pendingRedeploys.Remove(terminated.ActorRef, out var pending))
return;
// SiteRuntime-020: drop the name-keyed shadow now that the predecessor has
// Drop the name-keyed shadow now that the predecessor has
// fully terminated and its actor name is free. Any deploy arriving after
// this point sees neither _instanceActors[name] (cleared when we stopped
// the predecessor) nor _terminatingActorsByName[name] (cleared here), so
@@ -454,7 +454,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// Notify-and-fetch (Task 10): handles a small central→site
/// Notify-and-fetch: handles a small central→site
/// <see cref="RefreshDeploymentCommand"/>. Fetches the deployment's flattened config
/// from central over HTTP via <see cref="IDeploymentConfigFetcher"/>, then pipes the
/// result back to self so the existing apply path runs on the actor thread with the
@@ -491,7 +491,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// Notify-and-fetch (Task 10): the config fetch succeeded — reconstruct the in-process
/// Notify-and-fetch: the config fetch succeeded — reconstruct the in-process
/// <see cref="DeployInstanceCommand"/> apply DTO and reuse the existing apply path,
/// threading the original central sender through so the
/// <see cref="DeploymentStatusResponse"/> reaches it.
@@ -509,7 +509,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// Notify-and-fetch (Task 10): the config fetch failed — report
/// Notify-and-fetch: the config fetch failed — report
/// <see cref="DeploymentStatus.Failed"/> to the original central sender so the deploy
/// completes (rather than the central Ask hanging to timeout). Nothing is applied.
/// </summary>
@@ -528,7 +528,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// A redeployment is an update of an existing instance, so the deployed-instance
/// counter is only incremented for genuinely new deployments.
///
/// SiteRuntime-005: the deployer is <b>not</b> told <see cref="DeploymentStatus.Success"/>
/// The deployer is <b>not</b> told <see cref="DeploymentStatus.Success"/>
/// until SQLite persistence has committed. The site's deployed-config store is the
/// durable source of truth — a config that was never persisted would be silently lost
/// on the next restart/failover, so reporting Success before the row is committed is
@@ -569,7 +569,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
// config over HTTP itself, so a large config never crosses the intra-site Akka
// hop (which would silently drop on the 128 KB frame trap). When the coords are
// absent (deploy paths other than RefreshDeployment), the standby fetch is a
// no-op miss and T18 reconciliation is the durable backstop.
// no-op miss and reconciliation is the durable backstop.
_replicationActor?.Tell(new ReplicateConfigDeploy(
instanceName, command.DeploymentId, command.RevisionHash, true,
command.CentralFetchBaseUrl ?? "", command.FetchToken ?? ""));
@@ -587,7 +587,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// SiteRuntime-005: reports the deployment outcome to central only after the
/// Reports the deployment outcome to central only after the
/// persistence result is known. On a persistence failure the Instance Actor that was
/// created optimistically is stopped and the deployed-instance counter rolled back,
/// so the in-memory state stays consistent with durable storage, and central is told
@@ -597,7 +597,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
{
if (result.Success)
{
// M1.6: operational `deployment` event — deploy succeeded.
// Operational `deployment` event — deploy succeeded.
LogDeploymentEvent("Info", result.InstanceName,
$"Instance {result.InstanceName} deployed (deploymentId={result.DeploymentId})");
@@ -614,7 +614,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
"Failed to persist deployment {DeploymentId} for {Instance}: {Error}",
result.DeploymentId, result.InstanceName, result.Error);
// M1.6: operational `deployment` event — deploy failed.
// Operational `deployment` event — deploy failed.
LogDeploymentEvent("Error", result.InstanceName,
$"Instance {result.InstanceName} deploy failed (deploymentId={result.DeploymentId})",
result.Error);
@@ -642,7 +642,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
{
var instanceName = command.InstanceUniqueName;
// SiteRuntime-029: a disable arriving mid-redeploy must cancel the buffered
// A disable arriving mid-redeploy must cancel the buffered
// redeploy. Otherwise HandleTerminated re-creates the Instance Actor and
// re-stores its config with isEnabled: true when the predecessor terminates,
// silently reverting the operator's disable back to enabled. Mirror the
@@ -677,7 +677,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
if (t.IsCompletedSuccessfully)
{
_replicationActor?.Tell(new ReplicateConfigSetEnabled(instanceName, false));
// M1.6: operational `deployment` event — disable succeeded.
// Operational `deployment` event — disable succeeded.
LogDeploymentEvent("Info", instanceName, $"Instance {instanceName} disabled");
}
else
@@ -732,7 +732,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
if (result.Error != null || result.Config == null)
{
var error = result.Error ?? $"No deployed config found for {instanceName}";
// M1.6: operational `deployment` event — enable failed.
// Operational `deployment` event — enable failed.
LogDeploymentEvent("Error", instanceName,
$"Instance {instanceName} enable failed", error);
result.OriginalSender.Tell(new InstanceLifecycleResponse(
@@ -746,7 +746,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
UpdateInstanceCounts();
// M1.6: operational `deployment` event — enable succeeded.
// Operational `deployment` event — enable succeeded.
LogDeploymentEvent("Info", instanceName, $"Instance {instanceName} enabled");
result.OriginalSender.Tell(new InstanceLifecycleResponse(
@@ -763,7 +763,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
{
var instanceName = command.InstanceUniqueName;
// SiteRuntime-029: a delete arriving while a redeploy is still terminating must
// A delete arriving while a redeploy is still terminating must
// be authoritative over the mid-redeploy bookkeeping. HandleDeploy already
// removed the instance from _instanceActors and buffered a PendingRedeploy
// keyed by the terminating ref. If we fall straight through to the
@@ -794,12 +794,12 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
_instanceActors.Remove(instanceName);
}
// SiteRuntime-032: the deployed count is derived from the authoritative set of
// The deployed count is derived from the authoritative set of
// deployed config names, so removing the name here decrements it. Correct for a
// live, mid-redeploy, OR DISABLED instance (a disabled instance has a config row
// but is absent from both in-memory maps); a delete for a never-deployed instance
// removes nothing and leaves the count unchanged. Supersedes SiteRuntime-029's
// map-presence gate, which leaked the count on disabled-instance deletes.
// removes nothing and leaves the count unchanged. Supersedes the earlier
// map-presence check, which leaked the count on disabled-instance deletes.
_deployedInstanceNames.Remove(instanceName);
UpdateInstanceCounts();
@@ -809,7 +809,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
if (t.IsCompletedSuccessfully)
{
_replicationActor?.Tell(new ReplicateConfigRemove(instanceName));
// M1.6: operational `deployment` event — delete succeeded.
// Operational `deployment` event — delete succeeded.
LogDeploymentEvent("Info", instanceName, $"Instance {instanceName} deleted");
}
else
@@ -831,7 +831,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// M1.6: fire-and-forget a <c>deployment</c> operational event to the optional
/// Fire-and-forget a <c>deployment</c> operational event to the optional
/// <see cref="ISiteEventLogger"/> on a deploy/enable/disable/delete outcome.
/// Resolved optionally and never awaited so a logging failure cannot affect the
/// deployment pipeline (matching the established ScriptActor/ScriptExecutionActor
@@ -855,7 +855,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// DeploymentManager-006: answers a central query for the instance's
/// Answers a central query for the instance's
/// currently-applied deployment identity. The site's deployed-config store
/// (SQLite) is the authoritative record — it covers both enabled and
/// disabled instances, and survives node restart/failover. If the instance
@@ -892,13 +892,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}).PipeTo(sender);
}
// ── T17 / D6 — OPC UA server-certificate trust ──
// ── OPC UA server-certificate trust ──
/// <summary>
/// The base cluster role every site node carries (in addition to its
/// per-site role <c>site-{SiteId}</c>). Used to enumerate the site nodes a
/// trust/remove must reach so node-a and node-b PKI stores stay in lock-step
/// across failover (D6). Matches the role string set in <c>NodeOptions.Role</c>
/// across failover. Matches the role string set in <c>NodeOptions.Role</c>
/// for site hosts (see <c>AkkaHostedService.BuildRoles</c>).
/// </summary>
private const string SiteClusterRole = "Site";
@@ -911,7 +911,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
private static readonly TimeSpan CertBroadcastTimeout = TimeSpan.FromSeconds(5);
/// <summary>
/// T17 / D6: broadcasts a trust to the per-node <see cref="CertStoreActor"/>
/// Broadcasts a trust to the per-node <see cref="CertStoreActor"/>
/// on every Up site node so both PKI stores receive the certificate.
/// </summary>
private void HandleTrustServerCert(TrustServerCertCommand command) =>
@@ -920,7 +920,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
$"trust cert {command.Thumbprint} (connection {command.ConnectionName})");
/// <summary>
/// T17 / D6: broadcasts a remove to the per-node <see cref="CertStoreActor"/>
/// Broadcasts a remove to the per-node <see cref="CertStoreActor"/>
/// on every Up site node so the certificate leaves both PKI stores.
/// </summary>
private void HandleRemoveServerCert(RemoveServerCertCommand command) =>
@@ -929,7 +929,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
$"remove cert {command.Thumbprint}");
/// <summary>
/// T17: lists this node's trusted + rejected PKI stores by asking the LOCAL
/// Lists this node's trusted + rejected PKI stores by asking the LOCAL
/// <see cref="CertStoreActor"/> (the singleton's own node). The list reflects
/// the active node's view; a trust broadcast keeps the standby in sync.
/// </summary>
@@ -1010,8 +1010,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// <summary>
/// Tracks the configuration last sent to the DCL for each connection name, keyed by
/// a hash of the connection's protocol/endpoints/credentials/failover count
/// (SiteRuntime-010). A name whose hash is unchanged is skipped; a name whose config
/// a hash of the connection's protocol/endpoints/credentials/failover count.
/// A name whose hash is unchanged is skipped; a name whose config
/// changed re-issues a <c>CreateConnectionCommand</c> so the DCL adopts the new
/// configuration instead of keeping a stale connection after a redeployment.
/// </summary>
@@ -1021,7 +1021,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// Sets up DCL connections from the flattened config. Idempotent on unchanged
/// configuration, but re-issues the create command when a connection's endpoint,
/// credentials, backup endpoint, or failover retry count has changed since it was
/// last sent (SiteRuntime-010).
/// last sent.
/// </summary>
private void EnsureDclConnections(string configJson)
{
@@ -1049,7 +1049,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// SiteRuntime-021: hash-guarded DCL connection push shared by the inline
/// Hash-guarded DCL connection push shared by the inline
/// per-instance path (<see cref="EnsureDclConnections(string)"/>) and the
/// system-wide artifact-deploy path (<see cref="HandleDeployArtifacts"/>).
/// Unchanged config is a no-op; a changed endpoint/credentials/backup/
@@ -1091,8 +1091,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// <summary>
/// Computes a stable hash over the configuration fields that affect how the DCL
/// connects, so a changed endpoint/credential/backup/failover count is detected
/// (SiteRuntime-010).
/// connects, so a changed endpoint/credential/backup/failover count is detected.
/// </summary>
private static string ComputeConnectionConfigHash(
Commons.Types.Flattening.ConnectionConfig connConfig) =>
@@ -1103,7 +1102,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
connConfig.FailoverRetryCount);
/// <summary>
/// SiteRuntime-021: field-based core so the system-wide artifact-deploy
/// Field-based core so the system-wide artifact-deploy
/// path (which carries protocol/config-json/backup-json/failover directly
/// on <see cref="Commons.Messages.Artifacts.DataConnectionArtifact"/>) can
/// share the same hash + skip-or-resend logic as the inline-config path.
@@ -1161,7 +1160,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
// ── Shared Script Loading ──
/// <summary>
/// SiteRuntime-008: reads and compiles all shared scripts on a background task so the
/// Reads and compiles all shared scripts on a background task so the
/// SQLite read and Roslyn compilation never block the singleton's mailbox thread. The
/// result is piped back as a <see cref="SharedScriptsLoaded"/> message, carrying the
/// enabled configs to resume staggered Instance Actor creation on the actor thread.
@@ -1201,7 +1200,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
else
{
// M2.11: set InstanceNotFound=true so the caller can distinguish
// Set InstanceNotFound=true so the caller can distinguish
// "not deployed on this site" from a deployed-but-empty instance.
_logger.LogWarning(
"Debug view subscribe for unknown instance {Instance}", request.InstanceUniqueName);
@@ -1228,7 +1227,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
else
{
// M2.11: set InstanceNotFound=true so the caller can distinguish
// Set InstanceNotFound=true so the caller can distinguish
// "not deployed on this site" from a deployed-but-empty instance.
_logger.LogWarning(
"Debug snapshot for unknown instance {Instance}", request.InstanceUniqueName);
@@ -1246,7 +1245,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
if (_instanceActors.TryGetValue(request.InstanceUniqueName, out var instanceActor))
{
// Convert to ScriptCallRequest and Ask the Instance Actor.
// Audit Log #23 (ParentExecutionId): carry the inbound request's
// (ParentExecutionId): carry the inbound request's
// ExecutionId down as ParentExecutionId so the routed script
// execution can record its spawner.
var scriptCall = new ScriptCallRequest(
@@ -1494,7 +1493,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// WP-33: Handles system-wide artifact deployment (shared scripts, external systems, etc.).
/// Handles system-wide artifact deployment (shared scripts, external systems, etc.).
/// Persists artifacts to SiteStorageService and recompiles shared scripts.
/// </summary>
private void HandleDeployArtifacts(DeployArtifactsCommand command)
@@ -1513,7 +1512,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
{
try
{
// WP-33: Store shared scripts and recompile
// Store shared scripts and recompile
if (command.SharedScripts != null)
{
foreach (var script in command.SharedScripts)
@@ -1521,12 +1520,12 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
await _storage.StoreSharedScriptAsync(script.Name, script.Code,
script.ParameterDefinitions, script.ReturnDefinition);
// WP-33: Shared scripts recompiled on update
// Shared scripts recompiled on update
_sharedScriptLibrary.CompileAndRegister(script.Name, script.Code);
}
}
// WP-33: Store external system definitions
// Store external system definitions
if (command.ExternalSystems != null)
{
foreach (var es in command.ExternalSystems)
@@ -1536,7 +1535,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
}
// WP-33: Store database connection definitions
// Store database connection definitions
if (command.DatabaseConnections != null)
{
foreach (var db in command.DatabaseConnections)
@@ -1546,7 +1545,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
}
// DeploymentManager-025 / SiteRuntime-031: notification lists and SMTP
// Notification lists and SMTP
// configuration are central-only — sites store-and-forward notifications
// to central and never deliver over SMTP. Central no longer ships these
// (the DeployArtifactsCommand fields stay for additive compatibility but
@@ -1566,7 +1565,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
dc.BackupConfigurationJson, dc.FailoverRetryCount);
}
// SiteRuntime-021: after the SQLite store, dispatch an
// After the SQLite store, dispatch an
// internal message back to the actor thread so the DCL
// push runs through EnsureDclConnection — keeping the
// _createdConnections hash cache mutation actor-thread-
@@ -1581,7 +1580,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
self.Tell(new ApplyArtifactDataConnectionsToDcl(command.DataConnections));
}
// DeploymentManager-025 / SiteRuntime-031: SMTP configuration is
// SMTP configuration is
// central-only and is never stored on a site (see the purge above).
// Replicate artifacts to standby node
@@ -1611,7 +1610,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
return;
}
// SiteRuntime-015: reuse the shared, host-configured logger factory
// Reuse the shared, host-configured logger factory
// instead of allocating (and leaking) a fresh LoggerFactory per instance.
var props = Props.Create(() => new InstanceActor(
instanceName,
@@ -1633,7 +1632,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
/// <summary>
/// SiteRuntime-021: actor-thread handler that pushes artifact-deploy data
/// Actor-thread handler that pushes artifact-deploy data
/// connection definitions to the DCL via the shared
/// <see cref="EnsureDclConnection"/> helper. Dispatched from
/// <see cref="HandleDeployArtifacts"/>'s off-thread Task so the
@@ -1676,7 +1675,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
/// <summary>
/// Internal message piped back once shared scripts have been compiled off-thread
/// (SiteRuntime-008). Carries the enabled configs so staggered Instance Actor
/// Carries the enabled configs so staggered Instance Actor
/// creation resumes on the actor thread.
/// </summary>
internal record SharedScriptsLoaded(
@@ -1696,20 +1695,20 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
internal record PendingRedeploy(DeployInstanceCommand Command, IActorRef OriginalSender);
/// <summary>
/// Notify-and-fetch (Task 10): piped back to self when the deployment's flattened
/// Notify-and-fetch: piped back to self when the deployment's flattened
/// config has been fetched from central. Carries the original central sender so the
/// reused apply path replies to it.
/// </summary>
private sealed record RefreshFetched(RefreshDeploymentCommand Cmd, string ConfigJson, IActorRef ReplyTo);
/// <summary>
/// Notify-and-fetch (Task 10): piped back to self when the config fetch failed.
/// Notify-and-fetch: piped back to self when the config fetch failed.
/// Carries the original central sender so a Failed status is reported to it.
/// </summary>
private sealed record RefreshFetchFailed(RefreshDeploymentCommand Cmd, string Error, IActorRef ReplyTo);
/// <summary>
/// SiteRuntime-021: internal message dispatched from
/// Internal message dispatched from
/// <see cref="HandleDeployArtifacts"/>'s off-thread persistence task back
/// onto the actor thread, so the DCL push (and its hash-cache mutation)
/// runs through <see cref="EnsureDclConnection"/> without crossing
@@ -25,13 +25,13 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
/// (loaded from FlattenedConfiguration + static overrides from SQLite).
///
/// The Instance Actor is the single source of truth for runtime instance state.
/// WP-24: All state mutations are serialized through the actor mailbox.
/// All state mutations are serialized through the actor mailbox.
/// Multiple Script Execution Actors run concurrently; state mutations through this actor.
///
/// WP-15/16: Creates child Script Actors and Alarm Actors on startup.
/// WP-22: Tell for tag value updates, attribute notifications, stream publishing.
/// Creates child Script Actors and Alarm Actors on startup.
/// Tell for tag value updates, attribute notifications, stream publishing.
/// Ask for CallScript, debug snapshot.
/// WP-25: Debug view backend — snapshot + stream subscription.
/// Debug view backend — snapshot + stream subscription.
/// </summary>
public class InstanceActor : ReceiveActor
{
@@ -68,7 +68,7 @@ public class InstanceActor : ReceiveActor
private readonly Dictionary<string, AlarmStateChanged> _latestAlarmEvents = new();
private FlattenedConfiguration? _configuration;
// MV-8: resolved attributes indexed by canonical name. The TagValueUpdate
// Resolved attributes indexed by canonical name. The TagValueUpdate
// ingest path is the highest-frequency message this actor handles, so the
// attribute lookup must be O(1) rather than a linear scan of
// _configuration.Attributes. Built once in the constructor from the
@@ -144,11 +144,11 @@ public class InstanceActor : ReceiveActor
{
foreach (var attr in _configuration.Attributes)
{
// MV-8: index resolved attributes for O(1) lookup on the hot
// Index resolved attributes for O(1) lookup on the hot
// TagValueUpdate ingest path (last-wins on duplicate names).
_resolvedAttributeByName[attr.CanonicalName] = attr;
// MV-7: a STATIC List attribute's default is the canonical JSON
// A STATIC List attribute's default is the canonical JSON
// array string. Decode it to a typed List<T> for in-memory reads
// so scripts see a real collection. Scalars store their raw
// string unchanged. A malformed List default decodes to null and
@@ -176,7 +176,7 @@ public class InstanceActor : ReceiveActor
// Handle static attribute writes
Receive<SetStaticAttributeCommand>(HandleSetStaticAttribute);
// SiteRuntime-019: the disable/enable lifecycle is owned entirely by the
// The disable/enable lifecycle is owned entirely by the
// Deployment Manager — DeploymentManagerActor.HandleDisable/HandleEnable
// stop or re-create the Instance Actor directly and reply to the caller.
// DisableInstanceCommand / EnableInstanceCommand are never routed to the
@@ -184,10 +184,10 @@ public class InstanceActor : ReceiveActor
// handlers were dead code that implied a non-existent instance-side
// acknowledgement contract.)
// WP-15: Handle script call requests — route to appropriate Script Actor (Ask pattern)
// Handle script call requests — route to appropriate Script Actor (Ask pattern)
Receive<ScriptCallRequest>(HandleScriptCallRequest);
// WP-22/23: Handle attribute value changes from DCL (Tell pattern)
// Handle attribute value changes from DCL (Tell pattern)
Receive<AttributeValueChanged>(HandleAttributeValueChanged);
// WaitForAttribute (spec §4.2): event-driven "wait for value" waiter
@@ -207,16 +207,16 @@ public class InstanceActor : ReceiveActor
Receive<SubscribeTagsResponse>(_ => { }); // Ack from DCL subscribe — no action needed
Receive<ConnectionQualityChanged>(HandleConnectionQualityChanged);
// WP-16: Handle alarm state changes from Alarm Actors (Tell pattern)
// Handle alarm state changes from Alarm Actors (Tell pattern)
Receive<AlarmStateChanged>(HandleAlarmStateChanged);
// SiteRuntime-027: a NativeAlarmActor tells us when one of its native
// A NativeAlarmActor tells us when one of its native
// conditions has left the mirror for good (snapshot-swap removal, retention
// drop, or cap eviction) so we can evict the stale _latestAlarmEvents key
// and not leak per-instance memory / bloat DebugView snapshots.
Receive<NativeAlarmDropped>(HandleNativeAlarmDropped);
// WP-25: Debug view subscribe/unsubscribe (Ask pattern for snapshot)
// Debug view subscribe/unsubscribe (Ask pattern for snapshot)
Receive<SubscribeDebugViewRequest>(HandleSubscribeDebugView);
Receive<UnsubscribeDebugViewRequest>(HandleUnsubscribeDebugView);
@@ -233,7 +233,7 @@ public class InstanceActor : ReceiveActor
base.PreStart();
_logger.LogInformation("InstanceActor started for {Instance}", _instanceUniqueName);
// M1.6: operational `instance_lifecycle` event — instance started.
// Operational `instance_lifecycle` event — instance started.
// An instance starts on deploy, on enable (DeploymentManager re-creates
// the actor), and on failover/restart; this single point covers them all.
LogLifecycleEvent($"Instance {_instanceUniqueName} started");
@@ -257,7 +257,7 @@ public class InstanceActor : ReceiveActor
/// <inheritdoc />
protected override void PostStop()
{
// M1.6: operational `instance_lifecycle` event — instance stopped. An
// Operational `instance_lifecycle` event — instance stopped. An
// instance stops on disable, delete, redeployment, and graceful shutdown;
// this single point covers them all.
LogLifecycleEvent($"Instance {_instanceUniqueName} stopped");
@@ -265,7 +265,7 @@ public class InstanceActor : ReceiveActor
}
/// <summary>
/// M1.6: fire-and-forget an <c>instance_lifecycle</c> operational event to the
/// Fire-and-forget an <c>instance_lifecycle</c> operational event to the
/// optional <see cref="ISiteEventLogger"/>. Resolved optionally and never
/// awaited so a logging failure cannot affect the instance lifecycle
/// (matching the established ScriptActor/ScriptExecutionActor pattern).
@@ -311,7 +311,7 @@ public class InstanceActor : ReceiveActor
/// <summary>
/// Handles an attribute write (<c>Instance.SetAttribute</c> / Inbound API).
/// WP-24: State mutation serialized through this actor's mailbox.
/// State mutation serialized through this actor's mailbox.
///
/// The write is routed by the attribute's data binding:
/// * Data-sourced attribute → forwards a <see cref="WriteTagRequest"/> to the
@@ -330,7 +330,7 @@ public class InstanceActor : ReceiveActor
var resolved = _configuration?.Attributes
.FirstOrDefault(a => a.CanonicalName == command.AttributeName);
// SiteRuntime-025: reject writes targeting an attribute that does not exist
// Reject writes targeting an attribute that does not exist
// on the deployed instance. Without this check, an inbound API
// SetAttribute("notARealAttr", ...) would pollute the in-memory
// _attributes dictionary, publish a synthetic AttributeValueChanged to
@@ -373,7 +373,7 @@ public class InstanceActor : ReceiveActor
/// </summary>
private void HandleSetStaticAttributeCore(SetStaticAttributeCommand command)
{
// MV-7: command.Value is the canonical form — a plain string for scalars,
// command.Value is the canonical form — a plain string for scalars,
// a JSON array string for List attributes. For a List attribute we store
// the DECODED typed list in memory (so scripts read a real collection) but
// persist + publish the canonical JSON string UNCHANGED below. Scalars
@@ -382,7 +382,7 @@ public class InstanceActor : ReceiveActor
if (_resolvedAttributeByName.TryGetValue(command.AttributeName, out var resolved)
&& IsListAttribute(resolved))
{
// MV-7: the script path pre-encodes valid canonical JSON via ScopeAccessors,
// The script path pre-encodes valid canonical JSON via ScopeAccessors,
// but the Inbound API / direct-command path can submit an arbitrary
// command.Value. A non-empty value that fails to decode (malformed JSON,
// bad element, missing element type) is poison: storing it would null the
@@ -415,7 +415,7 @@ public class InstanceActor : ReceiveActor
_attributes[command.AttributeName] = command.Value;
}
// Publish attribute change to stream (WP-23) and notify children
// Publish attribute change to stream and notify children
var changed = new AttributeValueChanged(
_instanceUniqueName,
command.AttributeName,
@@ -469,7 +469,7 @@ public class InstanceActor : ReceiveActor
return;
}
// MV (C1): for a data-sourced List attribute the incoming command.Value is
// MV: for a data-sourced List attribute the incoming command.Value is
// the canonical JSON array string (ScopeAccessors encodes the script's
// List<T> for transport/storage). Writing that string straight to the DCL
// would push a String scalar to an array node. Decode it back to a typed
@@ -632,8 +632,8 @@ public class InstanceActor : ReceiveActor
}
/// <summary>
/// WP-15: Routes script call requests to the appropriate Script Actor.
/// Uses Ask pattern (WP-22).
/// Routes script call requests to the appropriate Script Actor.
/// Uses Ask pattern.
/// </summary>
private void HandleScriptCallRequest(ScriptCallRequest request)
{
@@ -641,7 +641,7 @@ public class InstanceActor : ReceiveActor
{
// Forward the request to the Script Actor, preserving the original
// sender. The whole record is forwarded unchanged, so any
// ParentExecutionId (Audit Log #23) set by an inbound-API-routed
// ParentExecutionId set by an inbound-API-routed
// call is carried through to the Script Actor verbatim.
scriptActor.Forward(request);
}
@@ -656,12 +656,12 @@ public class InstanceActor : ReceiveActor
}
/// <summary>
/// WP-22/23: Handles attribute value changes from DCL or static writes.
/// Handles attribute value changes from DCL or static writes.
/// Updates in-memory state, publishes to stream, and notifies children.
/// </summary>
private void HandleAttributeValueChanged(AttributeValueChanged changed)
{
// WP-24: State mutation serialized through this actor
// State mutation serialized through this actor
_attributes[changed.AttributeName] = changed.Value;
_attributeQualities[changed.AttributeName] = changed.Quality;
_attributeTimestamps[changed.AttributeName] = changed.Timestamp;
@@ -791,10 +791,10 @@ public class InstanceActor : ReceiveActor
// we resolve and convert per attribute rather than once for the tag.
foreach (var attrName in attrNames)
{
// MV-8: O(1) lookup off the hot ingest path (was a linear FirstOrDefault).
// O(1) lookup off the hot ingest path (was a linear FirstOrDefault).
_resolvedAttributeByName.TryGetValue(attrName, out var resolved);
// MV-8: a List-typed attribute coerces the incoming OPC UA array
// A List-typed attribute coerces the incoming OPC UA array
// (a CLR array/IEnumerable from the SDK) into a typed List<T>. On an
// element-type mismatch we set the attribute's quality to Bad, log a
// warning, and skip storing a value rather than crashing the actor.
@@ -845,12 +845,12 @@ public class InstanceActor : ReceiveActor
&& dt == DataType.List;
/// <summary>
/// MV-7: decodes a STATIC (authored / overridden) attribute's canonical value
/// Decodes a STATIC (authored / overridden) attribute's canonical value
/// for in-memory storage. List attributes carry a canonical JSON array string
/// (config default or persisted override) which is decoded via
/// <see cref="AttributeValueCodec.Decode"/> into a typed <c>List&lt;T&gt;</c>
/// so scripts read a real collection; scalars pass through unchanged. This is
/// the authored counterpart to MV-8's <see cref="TryCoerceListValue"/> (which
/// the authored counterpart to <see cref="TryCoerceListValue"/> (which
/// coerces live OPC UA CLR arrays). An undecodable List value (malformed JSON,
/// bad element, missing element type) degrades to <see langword="null"/> + a
/// warning — the caller marks the attribute Bad quality. NEVER throws into the
@@ -879,7 +879,7 @@ public class InstanceActor : ReceiveActor
}
/// <summary>
/// MV-8: coerces an incoming data-sourced value (an OPC UA array / IEnumerable)
/// Coerces an incoming data-sourced value (an OPC UA array / IEnumerable)
/// into a typed <c>List&lt;elementClrType&gt;</c> matching the attribute's
/// <see cref="ResolvedAttribute.ElementDataType"/>. Each element is converted
/// with invariant culture (round-trip parse for DateTime). Returns
@@ -1008,7 +1008,7 @@ public class InstanceActor : ReceiveActor
}
/// <summary>
/// WP-16: Handles alarm state changes from Alarm Actors.
/// Handles alarm state changes from Alarm Actors.
/// Updates in-memory alarm state and publishes to stream.
/// </summary>
private void HandleAlarmStateChanged(AlarmStateChanged changed)
@@ -1019,12 +1019,12 @@ public class InstanceActor : ReceiveActor
// DebugView snapshot reflects it — native alarms have no _alarmActors entry.
_latestAlarmEvents[changed.AlarmName] = changed;
// WP-23: Publish to site-wide stream
// Publish to site-wide stream
_streamManager?.PublishAlarmStateChanged(changed);
}
/// <summary>
/// SiteRuntime-027: evicts a native condition's key from the alarm-state maps once
/// Evicts a native condition's key from the alarm-state maps once
/// the owning <see cref="NativeAlarmActor"/> has dropped it from its mirror (after
/// emitting the condition's final return-to-normal). Without this the
/// <c>_latestAlarmEvents</c> map grows without bound on a source that mints a fresh
@@ -1043,7 +1043,7 @@ public class InstanceActor : ReceiveActor
}
/// <summary>
/// WP-25: Debug view subscribe — returns snapshot and begins streaming.
/// Debug view subscribe — returns snapshot and begins streaming.
/// </summary>
private void HandleSubscribeDebugView(SubscribeDebugViewRequest request)
{
@@ -1123,7 +1123,7 @@ public class InstanceActor : ReceiveActor
}
/// <summary>
/// WP-25: Debug view unsubscribe (SiteRuntime-013).
/// Debug view unsubscribe.
/// This handler is a deliberate no-op acknowledgement: the Instance Actor holds
/// no per-subscriber state. The real debug-stream subscription lifecycle lives in
/// <see cref="ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming.SiteStreamManager"/>
@@ -1164,7 +1164,7 @@ public class InstanceActor : ReceiveActor
/// <summary>
/// Publishes attribute change to stream and notifies child Script/Alarm actors.
/// WP-22: Tell for attribute notifications (fire-and-forget, never blocks).
/// Tell for attribute notifications (fire-and-forget, never blocks).
/// </summary>
/// <param name="changed">The attribute change to publish.</param>
/// <param name="evaluateWaiters">
@@ -1178,7 +1178,7 @@ public class InstanceActor : ReceiveActor
/// </param>
private void PublishAndNotifyChildren(AttributeValueChanged changed, bool evaluateWaiters = true)
{
// WP-23: Publish to site-wide stream
// Publish to site-wide stream
_streamManager?.PublishAttributeValueChanged(changed);
// Notify Script Actors (for value-change and conditional triggers)
@@ -1286,14 +1286,14 @@ public class InstanceActor : ReceiveActor
foreach (var kvp in result.Overrides)
{
// MV-7: persisted override values are canonical strings — a JSON array
// Persisted override values are canonical strings — a JSON array
// string for List attributes, a plain string for scalars. Decode List
// overrides to a typed list (matching the config-default load), set
// Bad quality on a malformed stored value, and never crash the actor.
if (_resolvedAttributeByName.TryGetValue(kvp.Key, out var resolved)
&& IsListAttribute(resolved))
{
// NJ-4: decode the stored List override (both old array-of-strings
// Decode the stored List override (both old array-of-strings
// and native-typed forms decode) and re-persist the native form if
// the stored value is still in the OLD form. Re-encoding the decoded
// list and comparing to the stored string detects old-form values
@@ -1334,11 +1334,11 @@ public class InstanceActor : ReceiveActor
/// <summary>
/// Creates child Script Actors and Alarm Actors from the flattened configuration.
/// WP-15: Script Actors spawned per script definition.
/// WP-16: Alarm Actors spawned per alarm definition, as peers to Script Actors.
/// WP-32: Compilation errors reject entire instance deployment (logged but actor still starts).
/// Script Actors spawned per script definition.
/// Alarm Actors spawned per alarm definition, as peers to Script Actors.
/// Compilation errors reject entire instance deployment (logged but actor still starts).
///
/// SiteRuntime-017: each child is seeded from a private point-in-time snapshot
/// Each child is seeded from a private point-in-time snapshot
/// of <c>_attributes</c>, NOT the live dictionary. The snapshot is taken here on
/// the Instance Actor thread, so it is race-free; handing the live mutable
/// <see cref="System.Collections.Generic.Dictionary{TKey,TValue}"/> by reference
@@ -1349,7 +1349,7 @@ public class InstanceActor : ReceiveActor
{
if (_configuration == null) return;
// SiteRuntime-017: snapshot the live attribute dictionary once, on the
// Snapshot the live attribute dictionary once, on the
// Instance Actor thread, before any child is constructed. Each child
// Props closure captures this immutable copy instead of the mutable
// _attributes field, so no child constructor ever enumerates a
@@ -1395,7 +1395,7 @@ public class InstanceActor : ReceiveActor
foreach (var alarm in _configuration.Alarms)
{
Script<object?>? onTriggerScript = null;
// M2.5 (#9): the on-trigger script's per-script execution timeout,
// The on-trigger script's per-script execution timeout,
// captured from its ResolvedScript so the AlarmExecutionActor can
// apply perScript ?? global. Null when there is no on-trigger script.
int? onTriggerTimeoutSeconds = null;
@@ -1441,7 +1441,7 @@ public class InstanceActor : ReceiveActor
attributeSnapshot,
_healthCollector,
_serviceProvider,
// M2.5 (#9): per-script timeout for the alarm on-trigger script.
// Per-script timeout for the alarm on-trigger script.
onTriggerTimeoutSeconds));
var actorRef = Context.ActorOf(props, $"alarm-{alarm.CanonicalName}");
@@ -14,7 +14,7 @@ using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
/// <summary>
/// Task 15: Native Alarm Actor — child of Instance Actor, peer to the computed
/// Native Alarm Actor — child of Instance Actor, peer to the computed
/// <see cref="AlarmActor"/>. Mirrors a single source binding's native alarms
/// read-only: subscribes through the DCL, applies snapshot/live transitions,
/// retains active+unacked conditions, persists to site SQLite, and emits an
@@ -41,7 +41,7 @@ public class NativeAlarmActor : ReceiveActor
private readonly IServiceProvider? _serviceProvider;
/// <summary>
/// M1.5: severity at or above which a native-alarm raise is logged as
/// Severity at or above which a native-alarm raise is logged as
/// <c>Error</c> to the site event log; below it, raises log as <c>Warning</c>.
/// Mirrors the 01000 condition-severity scale.
/// </summary>
@@ -66,7 +66,7 @@ public class NativeAlarmActor : ReceiveActor
/// <param name="nativeKind">Alarm kind to stamp on emitted events (OPC UA vs MxAccess); set by the
/// Instance Actor from the connection protocol. Defaults to <see cref="AlarmKind.NativeOpcUa"/>.</param>
/// <param name="serviceProvider">Optional DI service provider used to resolve the optional
/// <see cref="ISiteEventLogger"/> for M1.5 <c>alarm</c> operational events. Fire-and-forget;
/// <see cref="ISiteEventLogger"/> for <c>alarm</c> operational events. Fire-and-forget;
/// a logging failure never affects the mirror.</param>
public NativeAlarmActor(
ResolvedNativeAlarmSource source,
@@ -166,7 +166,7 @@ public class NativeAlarmActor : ReceiveActor
condition, string.Empty, string.Empty, string.Empty, string.Empty, string.Empty,
null, row.LastTransitionAt, string.Empty, string.Empty);
_alarms[row.SourceReference] = t;
// M1.5: rehydration replays last-known state on (re)start — surface it
// Rehydration replays last-known state on (re)start — surface it
// upward for the DebugView but do NOT re-log it as a fresh operational
// event (it is not a live transition).
Emit(t, t.Condition, logSiteEvent: false);
@@ -205,7 +205,7 @@ public class NativeAlarmActor : ReceiveActor
{
Emit(prior, prior.Condition with { Active = false });
PersistDelete(sourceRef);
// SiteRuntime-027: this condition is gone for good — tell the parent
// This condition is gone for good — tell the parent
// to evict its _latestAlarmEvents key so it does not retain a stale
// (Normal) entry forever.
NotifyParentDropped(sourceRef);
@@ -217,7 +217,7 @@ public class NativeAlarmActor : ReceiveActor
{
_alarms[sourceRef] = t;
PersistUpsert(t);
// M1.5: a snapshot replay is a re-sync of the source's current
// A snapshot replay is a re-sync of the source's current
// active set on (re)subscribe, NOT a live transition — surface it
// upward for the DebugView but do NOT re-log an `alarm` operational
// event. Otherwise every DCL reconnect would re-emit an `alarm`
@@ -249,7 +249,7 @@ public class NativeAlarmActor : ReceiveActor
{
_alarms.Remove(t.SourceReference);
PersistDelete(t.SourceReference);
// SiteRuntime-027: evict the parent's _latestAlarmEvents key for the
// Evict the parent's _latestAlarmEvents key for the
// now-resolved condition so it does not leak.
NotifyParentDropped(t.SourceReference);
}
@@ -303,7 +303,7 @@ public class NativeAlarmActor : ReceiveActor
{
var sourceRef = evicted.SourceReference;
// SiteRuntime-028: the sibling drop paths (ApplySnapshotSwap, the
// The sibling drop paths (ApplySnapshotSwap, the
// ApplyLiveTransition retention drop) always emit a return-to-normal
// before the condition leaves the mirror. EnforceCap previously dropped
// a condition whose last-emitted state could still be Active, with no
@@ -319,7 +319,7 @@ public class NativeAlarmActor : ReceiveActor
_alarms.Remove(sourceRef);
PersistDelete(sourceRef);
// SiteRuntime-027: this condition is gone for good — evict the parent's
// This condition is gone for good — evict the parent's
// _latestAlarmEvents key so it does not retain a stale entry.
NotifyParentDropped(sourceRef);
_logger.LogWarning(
@@ -329,7 +329,7 @@ public class NativeAlarmActor : ReceiveActor
}
/// <summary>
/// SiteRuntime-027: signals the parent Instance Actor that a native condition has
/// Signals the parent Instance Actor that a native condition has
/// left the mirror for good so it can evict the matching <c>_latestAlarmEvents</c>
/// key. Always sent AFTER the condition's final return-to-normal
/// <see cref="AlarmStateChanged"/> emit, so the stream still sees the clear.
@@ -345,7 +345,7 @@ public class NativeAlarmActor : ReceiveActor
/// <param name="t">The mirrored transition.</param>
/// <param name="condition">The condition state to surface (may differ from <paramref name="t"/>'s
/// own condition, e.g. a synthesised return-to-normal on snapshot swap).</param>
/// <param name="logSiteEvent">M1.5: when <c>true</c> (live + snapshot transitions), emit an
/// <param name="logSiteEvent">When <c>true</c> (live + snapshot transitions), emit an
/// <c>alarm</c> operational event. Suppressed for SQLite rehydration so a node restart does not
/// re-log every last-known condition.</param>
private void Emit(NativeAlarmTransition t, AlarmConditionState condition, bool logSiteEvent = true)
@@ -380,7 +380,7 @@ public class NativeAlarmActor : ReceiveActor
}
/// <summary>
/// M1.5: fire-and-forget an <c>alarm</c> operational event mirroring a native
/// Fire-and-forget an <c>alarm</c> operational event mirroring a native
/// condition transition. An active condition is a raise (severity by the
/// condition's severity); an inactive condition is a return-to-normal; an
/// acknowledge transition is informational. Resolved optionally and never
@@ -14,7 +14,7 @@ using System.Text.Json;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
/// <summary>
/// WP-15: Script Actor — coordinator actor, child of Instance Actor.
/// Script Actor — coordinator actor, child of Instance Actor.
/// Holds compiled script delegate, manages trigger configuration, and spawns
/// ScriptExecutionActor children per invocation. Does not block on child completion.
///
@@ -45,7 +45,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
private TimeSpan? _minTimeBetweenRuns;
/// <summary>
/// M2.5 (#9): the per-script execution timeout in seconds, or null to use the
/// The per-script execution timeout in seconds, or null to use the
/// global default. Threaded down to each spawned <see cref="ScriptExecutionActor"/>,
/// which applies <c>perScript ?? global</c> (and treats ≤ 0 as "use global").
/// </summary>
@@ -69,7 +69,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
private const string WhileTrueTimerKey = "whiletrue-trigger";
/// <summary>
/// SiteRuntime-017: the exact dictionary instance this actor was seeded from
/// The exact dictionary instance this actor was seeded from
/// at construction. The Instance Actor must pass a private snapshot here, not
/// its live <c>_attributes</c> field — sharing the live dictionary lets this
/// constructor enumerate it while the Instance Actor mutates it on another
@@ -207,9 +207,9 @@ public class ScriptActor : ReceiveActor, IWithTimers
return;
}
// Audit Log #23 (ParentExecutionId): carry any inbound-routed
// ParentExecutionId through to the ScriptExecutionActor so the routed
// script's ScriptRuntimeContext can record its spawner. Null for normal
// (ParentExecutionId): carry any inbound-routed ParentExecutionId
// through to the ScriptExecutionActor so the routed script's
// ScriptRuntimeContext can record its spawner. Null for normal
// (tag-change / timer) runs and nested Script.Call invocations.
SpawnExecution(
request.Parameters, request.CurrentCallDepth, Sender, request.CorrelationId,
@@ -413,7 +413,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
{
var executionId = $"{_scriptName}-exec-{_executionCounter++}";
// SiteRuntime-009: the actor's mailbox stays on the default dispatcher, but the
// The actor's mailbox stays on the default dispatcher, but the
// script body itself runs on the dedicated ScriptExecutionScheduler (a bounded
// set of dedicated threads), so blocking script I/O is contained there and
// cannot starve the shared .NET thread pool.
@@ -432,10 +432,10 @@ public class ScriptActor : ReceiveActor, IWithTimers
_scope,
_healthCollector,
_serviceProvider,
// Audit Log #23 (ParentExecutionId): null for trigger-driven runs;
// (ParentExecutionId): null for trigger-driven runs;
// an inbound-API-routed call supplies the inbound request's id.
parentExecutionId,
// M2.5 (#9): per-script timeout override (null = use global).
// Per-script timeout override (null = use global).
_executionTimeoutSeconds));
Context.ActorOf(props, executionId);
@@ -455,7 +455,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
try
{
// Use InvariantCulture so a string attribute value like "1.5" parses
// consistently regardless of the host locale (SiteRuntime-023). For
// consistently regardless of the host locale. For
// purely-numeric inputs the culture argument is a no-op, but it is
// safe and future-proof for string-typed attribute values arriving
// from scripts or the data connection layer.
@@ -14,17 +14,17 @@ using ZB.MOM.WW.ScadaBridge.StoreAndForward;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
/// <summary>
/// WP-15: Script Execution Actor -- short-lived child of Script Actor.
/// Script Execution Actor -- short-lived child of Script Actor.
/// Receives compiled code, params, Instance Actor ref, and call depth.
/// Executes the script via Script Runtime API, returns result, then stops.
///
/// The actor itself and its mailbox run on the default Akka dispatcher; only the
/// script body is dispatched off the actor thread, onto the dedicated
/// <see cref="ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptExecutionScheduler"/>
/// (SiteRuntime-009), so blocking script I/O cannot starve the shared thread pool
/// <see cref="ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptExecutionScheduler"/>,
/// so blocking script I/O cannot starve the shared thread pool
/// or stall other Akka dispatchers.
///
/// WP-32: Script failures are logged but do not disable the script.
/// Script failures are logged but do not disable the script.
/// Supervision: Stop on unhandled exception (parent ScriptActor decides).
/// </summary>
public class ScriptExecutionActor : ReceiveActor
@@ -47,7 +47,7 @@ public class ScriptExecutionActor : ReceiveActor
/// <param name="healthCollector">Optional health collector for recording execution metrics.</param>
/// <param name="serviceProvider">Optional DI service provider for script execution services.</param>
/// <param name="parentExecutionId">ExecutionId of the spawning inbound-API execution for audit correlation; null for normal runs.</param>
/// <param name="executionTimeoutSeconds">M2.5 (#9): per-script execution timeout in seconds. Null or non-positive falls back to the global <see cref="SiteRuntimeOptions.ScriptExecutionTimeoutSeconds"/>.</param>
/// <param name="executionTimeoutSeconds">Per-script execution timeout in seconds. Null or non-positive falls back to the global <see cref="SiteRuntimeOptions.ScriptExecutionTimeoutSeconds"/>.</param>
public ScriptExecutionActor(
string scriptName,
string instanceName,
@@ -63,11 +63,11 @@ public class ScriptExecutionActor : ReceiveActor
Commons.Types.Scripts.ScriptScope scope,
ISiteHealthCollector? healthCollector = null,
IServiceProvider? serviceProvider = null,
// Audit Log #23 (ParentExecutionId): the spawning execution's
// The spawning execution's
// ExecutionId for an inbound-API-routed call. Null for normal
// (tag-change / timer) runs and nested Script.Call invocations.
Guid? parentExecutionId = null,
// M2.5 (#9): per-script execution timeout override (seconds). Null or
// Per-script execution timeout override (seconds). Null or
// non-positive falls back to the global ScriptExecutionTimeoutSeconds.
int? executionTimeoutSeconds = null)
{
@@ -102,14 +102,14 @@ public class ScriptExecutionActor : ReceiveActor
Guid? parentExecutionId,
int? executionTimeoutSeconds)
{
// M2.5 (#9): per-script timeout overrides the global default. A null or
// Per-script timeout overrides the global default. A null or
// non-positive per-script value (≤ 0) falls back to the global.
var timeout = TimeSpan.FromSeconds(
executionTimeoutSeconds is { } perScript && perScript > 0
? perScript
: options.ScriptExecutionTimeoutSeconds);
// SiteRuntime-009: run the script body on the dedicated script-execution
// Run the script body on the dedicated script-execution
// scheduler, not the shared .NET thread pool, so blocking script I/O cannot
// starve the global pool and stall Akka dispatchers / HTTP handling.
var scheduler = ScriptExecutionScheduler.Shared(options);
@@ -136,23 +136,23 @@ public class ScriptExecutionActor : ReceiveActor
// provider supplies the site id stamped on enqueued notifications.
StoreAndForwardService? storeAndForward = null;
var siteId = string.Empty;
// Audit Log #23 (M2 Bundle F): the writer is a singleton (FallbackAuditWriter
// The writer is a singleton (FallbackAuditWriter
// composes the SQLite hot-path + drop-oldest ring); null in tests / hosts
// that haven't called AddAuditLog, which the helper handles as a no-op.
IAuditWriter? auditWriter = null;
// Audit Log #23 (M3 Bundle A — Task A3): site-local tracking store
// Site-local tracking store
// backing Tracking.Status(id). Singleton; null in tests / hosts
// that haven't wired the store, which the helper handles by
// throwing on access.
IOperationTrackingStore? operationTrackingStore = null;
// Audit Log #23 (M3 Bundle F — Task F1): site-side cached-call
// Site-side cached-call
// telemetry forwarder. Singleton bound to the AuditLog
// composition root; null in tests / hosts that haven't called
// AddAuditLog, in which case the cached-call helpers degrade
// to the no-emission path (the underlying S&F handoff still
// happens and a TrackedOperationId is still returned).
ICachedCallTelemetryForwarder? cachedForwarder = null;
// SourceNode-stamping (Tasks 13/14): the local node name
// SourceNode-stamping: the local node name
// resolved from INodeIdentityProvider — node-a/node-b on site
// hosts. Null in tests / hosts that haven't registered the
// provider, in which case NotificationSubmit.SourceNode and
@@ -191,34 +191,34 @@ public class ScriptExecutionActor : ReceiveActor
// Notification Outbox (FU3): stamp the executing script onto outbound
// notifications using the Site Event Logging "Source" convention.
sourceScript: $"ScriptActor:{scriptName}",
// Audit Log #23 (M2 Bundle F): emit one ApiOutbound/ApiCall row per
// Emit one ApiOutbound/ApiCall row per
// ExternalSystem.Call. Writer is best-effort; failures are logged
// and swallowed inside the helper so the script's call path is
// never aborted by an audit failure.
auditWriter: auditWriter,
// Audit Log #23 (M3 Bundle A — Task A3): site-local tracking store
// Site-local tracking store
// backing Tracking.Status(id). Authoritative source of truth for
// cached-call status — read directly by the script API.
operationTrackingStore: operationTrackingStore,
// Audit Log #23 (M3 Bundle F — Task F1): cached-call telemetry
// Cached-call telemetry
// forwarder for ExternalSystem.CachedCall / Database.CachedWrite
// CachedSubmit emission + the immediate-success terminal-row
// emission. Best-effort: null degrades the helpers to a
// no-emission path; the S&F handoff and TrackedOperationId
// return are unaffected.
cachedForwarder: cachedForwarder,
// Audit Log #23 (ParentExecutionId): the spawning execution's
// The spawning execution's
// id for an inbound-API-routed call. The routed script still
// mints its own fresh ExecutionId — this records the spawner.
// Null for normal (tag-change / timer) runs.
parentExecutionId: parentExecutionId,
// SourceNode-stamping (Tasks 13/14): the local node name
// SourceNode-stamping: the local node name
// (node-a/node-b on a site) — threaded down so Notify.Send
// and the four cached-call telemetry constructors can stamp
// it onto NotificationSubmit.SourceNode and
// SiteCallOperational.SourceNode respectively.
sourceNode: sourceNode,
// M2.12 (#25): thread the singleton site event logger so
// Thread the singleton site event logger so
// recursion-limit violations at CallScript/CallShared emit a
// script Error site event in addition to ILogger.LogError.
siteEventLogger: siteEventLogger,
@@ -236,7 +236,7 @@ public class ScriptExecutionActor : ReceiveActor
Scope = scope
};
// M1.8: operational `script` event — execution started. Fire-and-forget
// Operational `script` event — execution started. Fire-and-forget
// (the `_ =` discards the task) so the event log can never block or
// fault the script's own run; mirrors the existing Error-path emit.
_ = siteEventLogger?.LogEventAsync(
@@ -251,7 +251,7 @@ public class ScriptExecutionActor : ReceiveActor
replyTo.Tell(new ScriptCallResult(correlationId, true, state.ReturnValue, null));
}
// M1.8: operational `script` event — execution completed successfully.
// Operational `script` event — execution completed successfully.
_ = siteEventLogger?.LogEventAsync(
"script", "Info", instanceName, $"ScriptActor:{scriptName}",
$"Script '{scriptName}' on instance '{instanceName}' completed");
@@ -265,7 +265,7 @@ public class ScriptExecutionActor : ReceiveActor
var errorMsg = $"Script '{scriptName}' on instance '{instanceName}' timed out after {timeout.TotalSeconds}s";
logger.LogWarning(errorMsg);
// WP-32: Failures recorded to site event log; script NOT disabled after failure.
// Failures recorded to site event log; script NOT disabled after failure.
_ = siteEventLogger?.LogEventAsync(
"script", "Error", instanceName, $"ScriptActor:{scriptName}", errorMsg);
@@ -279,7 +279,7 @@ public class ScriptExecutionActor : ReceiveActor
catch (Exception ex)
{
healthCollector?.IncrementScriptError();
// WP-32: Failures recorded to site event log; script NOT disabled after failure.
// Failures recorded to site event log; script NOT disabled after failure.
var errorMsg = $"Script '{scriptName}' on instance '{instanceName}' failed: {ex.Message}";
logger.LogError(ex, "Script execution failed: {Script} on {Instance}", scriptName, instanceName);
@@ -154,9 +154,9 @@ public class SiteReplicationActor : ReceiveActor
{
if (string.IsNullOrEmpty(msg.CentralFetchBaseUrl))
{
// The direct DeployInstanceCommand cross-cluster wire path was retired (Task 14).
// The direct DeployInstanceCommand cross-cluster wire path was retired.
// This guard is a defensive fallback: skip quietly rather than calling FetchAsync("")
// and logging a spurious error. T18 reconciliation backstops any missed writes.
// and logging a spurious error. Reconciliation backstops any missed writes.
_logger.LogDebug(
"No fetch coords for {Instance} (deployment {DeploymentId}) — skipping replicated fetch; T18 reconciliation is the backstop",
msg.InstanceName, msg.DeploymentId);
@@ -178,7 +178,7 @@ public class SiteReplicationActor : ReceiveActor
// Notify-and-fetch: the peer sent only the id, so the standby fetches the config
// itself (off-thread; best-effort fire-and-forget, matching the no-ack replication
// model). The guarded write only overwrites a strictly-older local row. A single
// fetch attempt — T18 reconciliation is the durable backstop for a lost fetch.
// fetch attempt — reconciliation is the durable backstop for a lost fetch.
_configFetcher.FetchAsync(msg.CentralFetchBaseUrl, msg.DeploymentId, msg.FetchToken, CancellationToken.None)
.ContinueWith(async t =>
{
@@ -259,7 +259,7 @@ public class SiteReplicationActor : ReceiveActor
foreach (var db in command.DatabaseConnections)
await _storage.StoreDatabaseConnectionAsync(db.Name, db.ConnectionString, db.MaxRetries, db.RetryDelay);
// DeploymentManager-025 / SiteRuntime-031: notification lists and SMTP
// Notification lists and SMTP
// configuration are central-only and are never persisted on a site.
// Mirror the primary apply path: purge any pre-fix rows (including the
// plaintext SMTP password) instead of writing the command's