docs(src): fix CommentChecker findings across src/ — 1,032 → 5 (all false positives)
Comment/XML-doc changes only, no code logic: - strip project-tracking references (WP*, P*-*, row-NN, #NN, Task N) from comments, keeping the explanatory rationale - add missing summaries, <param>, <returns>, <typeparam> and <inheritdoc/> - replace misused <inheritdoc/> with real docs; where <inheritdoc/> was combined with useful remarks, move the rationale into // comments - drop stale replicationActor <param> on DeploymentManagerActor ctor Remaining 5 are false positives (CWE-1236 x2, failures-1, year-0001, M365). Build 0 errors / 0 warnings.
This commit is contained in:
@@ -42,34 +42,34 @@ public class AlarmActor : ReceiveActor
|
||||
private readonly IServiceProvider? _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Script-execution scheduler seam (#18): the process-wide
|
||||
/// Script-execution scheduler seam: the process-wide
|
||||
/// <see cref="ScriptExecutionScheduler"/> when null, or an injected instance so this
|
||||
/// alarm's launched on-trigger scripts run on a caller-owned pool. Resolved lazily at
|
||||
/// each use so the null (host) path is unchanged.
|
||||
///
|
||||
/// <para>WP3.1: trigger-expression evaluation no longer uses this scheduler — see
|
||||
/// <see cref="_evalGate"/>. That split is the whole point of finding #4: an alarm whose
|
||||
/// <para>Trigger-expression evaluation no longer uses this scheduler — see
|
||||
/// <see cref="_evalGate"/>. That split is the whole point: an alarm whose
|
||||
/// Expression trigger should raise in milliseconds must not queue behind blocking
|
||||
/// script bodies.</para>
|
||||
/// </summary>
|
||||
private readonly ScriptExecutionScheduler? _scheduler;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1 (finding #4): the concurrency gate for trigger-expression evaluation, or null
|
||||
/// The concurrency gate for trigger-expression evaluation, or null
|
||||
/// for the process-wide <see cref="TriggerEvalGate.Shared"/>. Evaluations run as plain
|
||||
/// async work on the shared .NET thread pool behind this gate.
|
||||
/// </summary>
|
||||
private readonly TriggerEvalGate? _evalGate;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: on-trigger runs launched but not yet completed. Incremented at launch,
|
||||
/// On-trigger runs launched but not yet completed. Incremented at launch,
|
||||
/// decremented on <see cref="AlarmExecutionCompleted"/>, which every terminal path
|
||||
/// emits — including the launch-path catch. Touched only on the actor thread.
|
||||
/// </summary>
|
||||
private int _runsInFlight;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: when the last shed Warning site event was emitted for this alarm. Sheds are
|
||||
/// When the last shed Warning site event was emitted for this alarm. Sheds are
|
||||
/// always counted; the event is rate-limited to one per alarm per minute.
|
||||
/// </summary>
|
||||
private DateTimeOffset _lastShedEventUtc = DateTimeOffset.MinValue;
|
||||
@@ -130,8 +130,7 @@ public class AlarmActor : ReceiveActor
|
||||
private bool _evalPending;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId tag-cascade): the
|
||||
/// <see cref="AttributeValueChanged.SourceExecutionId"/> of the most recent
|
||||
/// The <see cref="AttributeValueChanged.SourceExecutionId"/> of the most recent
|
||||
/// change folded into <see cref="_attributeSnapshot"/>. Expression triggers
|
||||
/// evaluate a whole snapshot off the dispatcher, so there is no single
|
||||
/// "triggering change" in scope when the boolean result comes back — this
|
||||
@@ -150,8 +149,7 @@ public class AlarmActor : ReceiveActor
|
||||
internal IReadOnlyDictionary<string, object?>? SeedAttributesReference { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId tag-cascade): the
|
||||
/// <c>parentExecutionId</c> handed to the most recently spawned
|
||||
/// The <c>parentExecutionId</c> handed to the most recently spawned
|
||||
/// on-trigger run — i.e. the execution whose attribute
|
||||
/// write fired this alarm, or <c>null</c> when the firing change came from
|
||||
/// the Data Connection Layer (external data has no spawning execution).
|
||||
@@ -185,8 +183,8 @@ public class AlarmActor : ReceiveActor
|
||||
/// <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>
|
||||
/// <param name="scheduler">Optional script-execution scheduler override (#18); null uses the process-wide shared scheduler.</param>
|
||||
/// <param name="evalGate">Optional trigger-expression concurrency gate override (WP3.1); null uses the process-wide shared gate.</param>
|
||||
/// <param name="scheduler">Optional script-execution scheduler override; null uses the process-wide shared scheduler.</param>
|
||||
/// <param name="evalGate">Optional trigger-expression concurrency gate override; null uses the process-wide shared gate.</param>
|
||||
public AlarmActor(
|
||||
string alarmName,
|
||||
string instanceName,
|
||||
@@ -202,9 +200,9 @@ public class AlarmActor : ReceiveActor
|
||||
IServiceProvider? serviceProvider = null,
|
||||
// Per-script timeout for the on-trigger script (null = global).
|
||||
int? onTriggerExecutionTimeoutSeconds = null,
|
||||
// Script-execution scheduler seam (#18); null uses the process-wide shared scheduler.
|
||||
// Script-execution scheduler seam; null uses the process-wide shared scheduler.
|
||||
ScriptExecutionScheduler? scheduler = null,
|
||||
// WP3.1 trigger-eval gate seam; null uses the process-wide shared gate.
|
||||
// Trigger-eval gate seam; null uses the process-wide shared gate.
|
||||
TriggerEvalGate? evalGate = null)
|
||||
{
|
||||
_alarmName = alarmName;
|
||||
@@ -248,7 +246,7 @@ public class AlarmActor : ReceiveActor
|
||||
// Handle attribute value changes
|
||||
Receive<AttributeValueChanged>(HandleAttributeValueChanged);
|
||||
|
||||
// Handle alarm execution completion (also releases the WP3.1 in-flight slot)
|
||||
// Handle alarm execution completion (also releases the in-flight slot)
|
||||
Receive<AlarmExecutionCompleted>(HandleAlarmExecutionCompleted);
|
||||
|
||||
// Handle the off-dispatcher trigger-expression evaluation result (P1).
|
||||
@@ -268,7 +266,7 @@ public class AlarmActor : ReceiveActor
|
||||
_alarmName, _instanceName, _triggerType);
|
||||
}
|
||||
|
||||
// WP3.1: the Stop-on-failure SupervisorStrategy override is gone with the per-run
|
||||
// The Stop-on-failure SupervisorStrategy override is gone with the per-run
|
||||
// AlarmExecutionActor child it supervised. This actor has no children left; a
|
||||
// launch-path throw is caught in SpawnAlarmExecution so it can never escalate to
|
||||
// InstanceActor, which continues to supervise this actor with Resume exactly as before.
|
||||
@@ -613,7 +611,7 @@ public class AlarmActor : ReceiveActor
|
||||
// mis-attribute the raise this evaluation produces.
|
||||
var sourceExecutionId = _latestSourceExecutionId;
|
||||
var gate = _evalGate ?? TriggerEvalGate.Shared(_options);
|
||||
// WP3.1 (finding #4): the deadline clock starts AT ENQUEUE, on the actor thread —
|
||||
// The deadline clock starts AT ENQUEUE, on the actor thread —
|
||||
// gate-wait time burns the same budget, so a saturated gate yields a timely false
|
||||
// instead of an unbounded stall. And the work runs on the shared .NET thread pool,
|
||||
// NOT the blocking script pool, so it can never queue behind a blocked script body.
|
||||
@@ -747,7 +745,7 @@ public class AlarmActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: releases the in-flight slot for a completed on-trigger run. Every terminal
|
||||
/// Releases the in-flight slot for a completed on-trigger run. Every terminal
|
||||
/// path emits exactly one <see cref="AlarmExecutionCompleted"/> (success / timeout /
|
||||
/// failure / launch failure), so the counter tracks reality.
|
||||
/// </summary>
|
||||
@@ -760,7 +758,7 @@ public class AlarmActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: on-trigger runs launched but not yet completed. Exposed for regression
|
||||
/// On-trigger runs launched but not yet completed. Exposed for regression
|
||||
/// coverage of the shed cap.
|
||||
/// </summary>
|
||||
internal int RunsInFlight => _runsInFlight;
|
||||
@@ -770,7 +768,7 @@ public class AlarmActor : ReceiveActor
|
||||
/// Passes the firing alarm's level/priority/message so the script can
|
||||
/// branch on severity via the <c>Alarm</c> global.
|
||||
///
|
||||
/// <para>WP3.1: launched directly via <see cref="ScriptRunLauncher"/> rather than
|
||||
/// <para>Launched directly via <see cref="ScriptRunLauncher"/> rather than
|
||||
/// through a short-lived <c>AlarmExecutionActor</c> child, and bounded by
|
||||
/// <see cref="SiteRuntimeOptions.MaxConcurrentRunsPerScript"/> — a raise arriving while
|
||||
/// the cap is reached is shed (counted, rate-limited site event) rather than piling
|
||||
@@ -828,7 +826,7 @@ public class AlarmActor : ReceiveActor
|
||||
// Context.Parent.Tell from the execution actor.
|
||||
Self,
|
||||
_logger,
|
||||
// arch-review 2026-08-19 P1-3: same health collector + service provider as
|
||||
// Same health collector + service provider as
|
||||
// the instance-script path, so the alarm on-trigger run gets the same
|
||||
// stuck-script watchdog, script-error counting, and run-summary threading.
|
||||
_healthCollector,
|
||||
@@ -838,13 +836,13 @@ public class AlarmActor : ReceiveActor
|
||||
_onTriggerExecutionTimeoutSeconds,
|
||||
// The firing execution's id — null for DCL-originated changes.
|
||||
parentExecutionId,
|
||||
// Scheduler seam (#18): share this alarm's scheduler override with the
|
||||
// Scheduler seam: share this alarm's scheduler override with the
|
||||
// launched on-trigger script body (null = process-wide shared).
|
||||
_scheduler, Context.System.ActorSelection("/user/site-communication"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// WP3.1 supervision parity: mirrors the removed OneForOneStrategy's warn-and-stop
|
||||
// Supervision parity: mirrors the removed OneForOneStrategy's warn-and-stop
|
||||
// for a per-run child that failed to construct. The alarm continues.
|
||||
_logger.LogWarning(ex,
|
||||
"Alarm on-trigger execution launch for {Alarm} on {Instance} failed, stopping",
|
||||
@@ -854,7 +852,7 @@ public class AlarmActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1 shed policy for alarm on-trigger runs: refuses the newest run when
|
||||
/// Shed policy for alarm on-trigger runs: refuses the newest run when
|
||||
/// <see cref="SiteRuntimeOptions.MaxConcurrentRunsPerScript"/> are already in flight.
|
||||
/// There is no Ask caller on this path (an on-trigger run is never awaited), so the
|
||||
/// shed surfaces as a counter plus a rate-limited Warning site event.
|
||||
|
||||
@@ -43,7 +43,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
private readonly DeployCompileValidator _deployCompileValidator;
|
||||
private readonly SharedScriptLibrary _sharedScriptLibrary;
|
||||
/// <summary>
|
||||
/// WP2.6a: the shared cache backing <c>SiteExternalSystemRepository</c> reads.
|
||||
/// The shared cache backing <c>SiteExternalSystemRepository</c> reads.
|
||||
/// Invalidated wholesale after <see cref="HandleDeployArtifacts"/> applies
|
||||
/// external-system changes so a script's next call re-reads fresh definitions/method
|
||||
/// lists instead of serving a stale cached snapshot. Null in tests that do not wire
|
||||
@@ -51,7 +51,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// </summary>
|
||||
private readonly ExternalSystemDefinitionCache? _externalSystemCache;
|
||||
/// <summary>
|
||||
/// Task 10 (site-secrets artifact deploy): per-node mirror reconciled synchronously by
|
||||
/// Per-node mirror reconciled synchronously by
|
||||
/// <see cref="HandleDeployArtifacts"/> right after the shipped secrets are written to
|
||||
/// <c>deployed_secrets</c>, so the reply's <c>SecretsApplied</c> reflects this node's own
|
||||
/// apply/verify outcome rather than the next periodic pass. Optional/null on nodes/tests
|
||||
@@ -97,7 +97,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// test can hold the read open mid-flight (row 101's ensure-started race tests).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Register P1-4: the row-67 ensure-started heal used to read through
|
||||
/// The ensure-started heal used to read through
|
||||
/// <see cref="_configLoader"/> — the same full-table-including-ConfigJson load the boot
|
||||
/// sequence uses — just to <c>FirstOrDefault</c> one instance. A volume-restore reconcile
|
||||
/// that raises hundreds of <see cref="EnsureInstanceStartedCommand"/>s in a row turned that
|
||||
@@ -202,7 +202,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
private readonly HashSet<string> _initFailedPendingRowRemoval = new();
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1 warm-then-gate: instances whose deploy compile is being warmed off the actor
|
||||
/// Warm-then-gate: instances whose deploy compile is being warmed off the actor
|
||||
/// thread, keyed by instance name. Presence is the per-instance in-flight guard that keeps
|
||||
/// same-instance command ordering intact while OTHER instances' commands flow freely.
|
||||
/// Entries are added in <see cref="HandleDeploy"/> and removed in
|
||||
@@ -212,7 +212,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
private readonly Dictionary<string, DeployWarmState> _deployWarms = new();
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: script-execution scheduler seam (#18) — the pool this actor grows as instances
|
||||
/// Script-execution scheduler seam — the pool this actor grows as instances
|
||||
/// deploy. Null selects the process-wide shared scheduler; tests inject their own so
|
||||
/// sizing assertions do not perturb (or depend on) the process-wide singleton.
|
||||
/// </summary>
|
||||
@@ -236,7 +236,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// <summary>
|
||||
/// The stored revision hash the instance reconcile sweep has already ACTED on for an
|
||||
/// instance, keyed by instance name — <b>one repair attempt per (instance, revision)
|
||||
/// pair</b>, for both the row-78 start and the row-72 restart.
|
||||
/// pair</b>, for both the reconcile-sweep start and the snapshot-driven restart.
|
||||
/// <para>
|
||||
/// This is what keeps a periodic self-heal from becoming a restart loop. An instance whose
|
||||
/// Instance Actor dies during init leaves its config row in place, so without this the
|
||||
@@ -281,7 +281,6 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// <param name="options">Site runtime options (startup batch size, stagger interval, etc.).</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="dclManager">Optional Data Connection Layer manager actor.</param>
|
||||
/// <param name="replicationActor">Optional standby replication actor.</param>
|
||||
/// <param name="healthCollector">Optional site health collector.</param>
|
||||
/// <param name="serviceProvider">Optional service provider for resolving per-instance services.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory for creating Instance Actor loggers.</param>
|
||||
@@ -295,13 +294,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// <param name="configLoader">Optional override for loading all deployed configurations
|
||||
/// at startup; defaults to reading from <paramref name="storage"/>. Primarily for tests.</param>
|
||||
/// <param name="externalSystemCache">
|
||||
/// WP2.6a: the shared <see cref="ExternalSystemDefinitionCache"/> backing
|
||||
/// The shared <see cref="ExternalSystemDefinitionCache"/> backing
|
||||
/// <c>SiteExternalSystemRepository</c> reads, invalidated after this actor applies
|
||||
/// external-system changes. Optional/null in tests that do not exercise external-system
|
||||
/// caching.
|
||||
/// </param>
|
||||
/// <param name="scriptScheduler">
|
||||
/// WP3.1: optional script-execution scheduler override (#18). This actor grows the pool
|
||||
/// Optional script-execution scheduler override. This actor grows the pool
|
||||
/// towards <see cref="ScriptExecutionScheduler.ComputeTargetThreads"/> on every instance-
|
||||
/// count change; null uses the process-wide shared scheduler.
|
||||
/// </param>
|
||||
@@ -312,12 +311,12 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// drive a read failure.
|
||||
/// </param>
|
||||
/// <param name="deployedConfigLoader">
|
||||
/// Register P1-4: optional override for the single-row deployed-config read the row-67
|
||||
/// Optional override for the single-row deployed-config read the
|
||||
/// ensure-started heal uses; defaults to reading from <paramref name="storage"/>. Primarily
|
||||
/// for tests that need to hold the read open mid-flight to drive the row-101 race.
|
||||
/// for tests that need to hold the read open mid-flight to drive the freshness race.
|
||||
/// </param>
|
||||
/// <param name="secretsMirror">
|
||||
/// Task 10 (site-secrets artifact deploy): this node's <see cref="DeployedSecretsMirror"/>,
|
||||
/// This node's <see cref="DeployedSecretsMirror"/>,
|
||||
/// reconciled synchronously by <see cref="HandleDeployArtifacts"/> right after a shipped
|
||||
/// <c>command.Secrets</c> set is written to <c>deployed_secrets</c>. Optional/null on
|
||||
/// nodes/tests that never register secret deployment.
|
||||
@@ -378,10 +377,10 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
Receive<EnableInstanceCommand>(cmd => HandleEnable(cmd, Sender));
|
||||
Receive<DeleteInstanceCommand>(cmd => HandleDelete(cmd, Sender));
|
||||
|
||||
// WP3.1 warm-then-gate: the off-thread compile warm for a deploy has finished, so
|
||||
// Warm-then-gate: the off-thread compile warm for a deploy has finished, so
|
||||
// the (now all-cache-hit) synchronous compile gate can run on the actor thread.
|
||||
Receive<DeployCompileWarmed>(HandleDeployCompileWarmed);
|
||||
// WP3.1: a staggered startup batch's compiles have been pre-warmed off-thread;
|
||||
// A staggered startup batch's compiles have been pre-warmed off-thread;
|
||||
// create the batch's Instance Actors now that their PreStart compiles are hits.
|
||||
Receive<BatchCompileWarmed>(msg => CreateInstanceActorBatch(msg.Batch));
|
||||
|
||||
@@ -732,7 +731,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
// Rows 89 + 90: seed the DCL arbitration overlay BEFORE any Instance Actor is created,
|
||||
// so the boot batch's EnsureDclConnections already resolves to the definition-owned
|
||||
// config. This is the failover half of the row-89 fix: a singleton incarnation starting
|
||||
// config. This is the failover half of the fix: a singleton incarnation starting
|
||||
// on the peer node rebuilds the whole DCL from flattened snapshots, and without the
|
||||
// overlay in place first it would rebuild them STALE and only correct itself a sweep
|
||||
// tick later — a needless create-then-diverge on every failover.
|
||||
@@ -764,7 +763,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
if (msg.EnabledConfigs.Count == 0)
|
||||
{
|
||||
// Nothing to create, so boot-time creation is trivially complete and the instance
|
||||
// reconcile sweep may run. This is exactly the row-78 live case: the node booted
|
||||
// reconcile sweep may run. This is exactly the live case: the node booted
|
||||
// with an empty deployed_configurations table and CDC replication seeds it moments
|
||||
// later, with no boot load left to notice.
|
||||
_startupInstanceCreationComplete = true;
|
||||
@@ -777,7 +776,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: pre-warms one staggered startup batch's script and trigger-expression compiles
|
||||
/// Pre-warms one staggered startup batch's script and trigger-expression compiles
|
||||
/// off the actor thread, then pipes <see cref="BatchCompileWarmed"/> so
|
||||
/// <see cref="CreateInstanceActorBatch"/> can create the batch's Instance Actors with
|
||||
/// every <c>PreStart</c> compile already a cache hit.
|
||||
@@ -814,7 +813,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
catch
|
||||
{
|
||||
// Best-effort warm — a throw here just means that instance's PreStart
|
||||
// compiles the hard way, exactly as it did before WP3.1.
|
||||
// compiles the hard way, exactly as it did before the warm-then-gate change.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,7 +895,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
|
||||
// WP3.1 warm-then-gate. The S3 gate below must stay synchronous on the actor thread
|
||||
// Warm-then-gate. The S3 gate below must stay synchronous on the actor thread
|
||||
// (mailbox FIFO is what makes redeploy-supersede and delete-during-redeploy correct),
|
||||
// but the Roslyn compile it performs used to hold the whole singleton — every OTHER
|
||||
// instance's commands stalled behind one instance's compile. So the compile is WARMED
|
||||
@@ -941,7 +940,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// <see cref="DeployCompileWarmed"/> back so the authoritative gate can run on the actor
|
||||
/// thread against a warm cache. The warm is best-effort: its verdict is discarded and any
|
||||
/// exception swallowed, because <see cref="RunDeployGateAndProceed"/> re-derives the
|
||||
/// verdict (and reproduces any failure) exactly as it did before WP3.1.
|
||||
/// verdict (and reproduces any failure) exactly as it did before the warm-then-gate change.
|
||||
/// </summary>
|
||||
private void StartDeployCompileWarm(DeployInstanceCommand command)
|
||||
{
|
||||
@@ -1044,7 +1043,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// synchronously on the actor thread as a pure prefix step of the deploy
|
||||
// application, so the existing redeploy-supersede / delete-during-redeploy
|
||||
// ordering (which depends on strict mailbox FIFO) is preserved exactly.
|
||||
// WP3.1 removed the COST rather than the ordering: the same compile has
|
||||
// The warm-then-gate change removed the COST rather than the ordering: the same compile has
|
||||
// already been warmed off-thread (StartDeployCompileWarm), so this call is
|
||||
// pure cache hits and no longer holds the singleton — while the per-instance
|
||||
// warm guard keeps this instance's own command ordering intact. Redeploys and
|
||||
@@ -1345,7 +1344,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// Static overrides and mirrored native alarm state are reset on redeployment per
|
||||
// design decision — the new config may bind different sources, and the source snapshot
|
||||
// re-seeds the mirror on (re)subscribe. Dispatched HERE, BEFORE the successor exists,
|
||||
// and handed to it as a gate (arch-review 2026-08-19 P1-2). It used to run inside the
|
||||
// and handed to it as a gate. It used to run inside the
|
||||
// persistence task below, i.e. after the actor was already created AND queued behind a
|
||||
// full config-JSON write, so the successor's PreStart override load routinely won the
|
||||
// race and applied the PREDECESSOR's overrides — including keys absent from the new
|
||||
@@ -1557,7 +1556,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
var instanceName = command.InstanceUniqueName;
|
||||
|
||||
// WP3.1 warm-then-gate: a deploy for this instance is mid-compile-warm; queue this
|
||||
// Warm-then-gate: a deploy for this instance is mid-compile-warm; queue this
|
||||
// command so same-instance ordering is preserved (see TryBufferDuringDeployWarm).
|
||||
if (TryBufferDuringDeployWarm(instanceName, command, replyTo)) return;
|
||||
if (DeferLifecycleStop(instanceName, command.CommandId, false, replyTo)) return;
|
||||
@@ -1567,7 +1566,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
var sender = replyTo;
|
||||
|
||||
// Row 101 (initiation half): the actor has just stopped the Instance Actor while the
|
||||
// durable row still reads enabled, which is exactly the shape the row-78 sweep repairs.
|
||||
// durable row still reads enabled, which is exactly the shape the reconcile sweep repairs.
|
||||
// Any store read in flight would therefore "prove" a divergence that does not exist and
|
||||
// restart the instance the operator just disabled.
|
||||
BumpStoreWriteGeneration("disable dispatch");
|
||||
@@ -1617,7 +1616,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
|
||||
// WP3.1 warm-then-gate: see TryBufferDuringDeployWarm.
|
||||
// Warm-then-gate: see TryBufferDuringDeployWarm.
|
||||
if (TryBufferDuringDeployWarm(instanceName, command, replyTo)) return;
|
||||
|
||||
var sender = replyTo;
|
||||
@@ -1628,7 +1627,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await _storage.SetInstanceEnabledAsync(instanceName, true);
|
||||
// Row P1-4: a single-row read — this used to materialize every deployed config
|
||||
// A single-row read — this used to materialize every deployed config
|
||||
// (including every ConfigJson blob) just to FirstOrDefault this one instance.
|
||||
var config = await _storage.GetDeployedConfigAsync(instanceName);
|
||||
return new EnableResult(command, config, null, sender);
|
||||
@@ -1722,7 +1721,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
var generationAtRead = _storeWriteGeneration;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// Register P1-4: read a single row through _deployedConfigLoader (defaults to
|
||||
// Read a single row through _deployedConfigLoader (defaults to
|
||||
// _storage.GetDeployedConfigAsync — a WHERE instance_unique_name = @name read, not
|
||||
// the full-table-including-ConfigJson load the boot sequence uses), which still lets
|
||||
// a test hold the read open and land the result behind a committed disable/delete.
|
||||
@@ -1821,7 +1820,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
/// <summary>
|
||||
/// The one place an advisory self-heal turns a stored, enabled config into a running
|
||||
/// Instance Actor — shared by the row-67 ensure-started handler and the row-78 sweep so
|
||||
/// Instance Actor — shared by the ensure-started handler and the reconcile sweep so
|
||||
/// both produce identical bookkeeping and identical operator-facing surfacing. Callers
|
||||
/// are responsible for the guards; this method assumes they have passed.
|
||||
/// </summary>
|
||||
@@ -1915,7 +1914,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
// Row 101, sweep half: this node mutated one of the two tables while the read was in
|
||||
// flight, so the snapshot predates state the actor has already advanced past. Discard it
|
||||
// WHOLE — no overlay apply, no row-78 start, no row-72 restart. Every repair this sweep
|
||||
// WHOLE — no overlay apply, no sweep start, no snapshot restart. Every repair this sweep
|
||||
// makes is derived from "the store says X, the runtime says Y", and a snapshot older
|
||||
// than the runtime inverts that comparison: it would restart an instance BACKWARD onto
|
||||
// the revision a deploy just replaced, or resurrect one a disable/delete just stopped.
|
||||
@@ -2099,7 +2098,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
{
|
||||
var instanceName = command.InstanceUniqueName;
|
||||
|
||||
// WP3.1 warm-then-gate: see TryBufferDuringDeployWarm.
|
||||
// Warm-then-gate: see TryBufferDuringDeployWarm.
|
||||
if (TryBufferDuringDeployWarm(instanceName, command, replyTo)) return;
|
||||
if (DeferLifecycleStop(instanceName, command.CommandId, true, replyTo)) return;
|
||||
|
||||
@@ -2191,7 +2190,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
var sender = Sender;
|
||||
var instanceName = request.InstanceUniqueName;
|
||||
|
||||
// Row P1-4: a single-row, ConfigJson-free projection — this used to materialize every
|
||||
// A single-row, ConfigJson-free projection — this used to materialize every
|
||||
// deployed config (including every ConfigJson blob) just to FirstOrDefault this one
|
||||
// instance's DeploymentId/RevisionHash. Neither is ever read here.
|
||||
_storage.GetDeployedConfigMetadataAsync(instanceName).ContinueWith(t =>
|
||||
@@ -2493,13 +2492,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// <item>DEFINITIONS half: absences "prove" just-applied names disappeared, tombstoning and
|
||||
/// STOPPING live connections that nothing re-creates until an instance redeploy; rows push
|
||||
/// superseded configs onto live connections and flip-flop with the next fresh read.</item>
|
||||
/// <item>CONFIGS half, row-72 leg: a snapshot carrying the revision a deploy has just
|
||||
/// <item>CONFIGS half, deploy-revision leg: a snapshot carrying the revision a deploy has just
|
||||
/// replaced makes <see cref="TryRestartStaleInstance"/> "repair" the instance BACKWARD —
|
||||
/// it redeploys the snapshot's own (old) config JSON through the redeploy path.
|
||||
/// <see cref="InstanceIsOwnedByADeployPipeline"/> does not cover this: it only holds while
|
||||
/// the deploy is in flight, and the stale snapshot can land after the pipeline released
|
||||
/// ownership.</item>
|
||||
/// <item>CONFIGS half, row-78 leg: a snapshot read before an undeploy/delete or a disable
|
||||
/// <item>CONFIGS half, stale-snapshot leg: a snapshot read before an undeploy/delete or a disable
|
||||
/// committed still shows an enabled row while the actor is already stopped, so
|
||||
/// <see cref="StartStoredInstance"/> RESURRECTS a just-stopped instance.</item>
|
||||
/// </list>
|
||||
@@ -2543,7 +2542,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The row-101 freshness test both snapshot consumers run BEFORE touching anything the
|
||||
/// The freshness test both snapshot consumers run BEFORE touching anything the
|
||||
/// snapshot carries. <see langword="true"/> means this actor mutated one of the two tables
|
||||
/// while the read was in flight, so the snapshot predates state the actor has already
|
||||
/// advanced past and must be discarded whole (see <see cref="_storeWriteGeneration"/> for
|
||||
@@ -3231,7 +3230,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// M2 (final review): the DCL push for this deploy's data connections is captured
|
||||
// The DCL push for this deploy's data connections is captured
|
||||
// here and sent only AFTER the shipped secrets are written and mirrored (in the
|
||||
// finally below), so a connection whose config references a secret this same deploy
|
||||
// ships resolves it on its FIRST connect instead of failing "not found" and waiting
|
||||
@@ -3279,7 +3278,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
await _storage.DeleteExternalSystemsExceptAsync(
|
||||
command.ExternalSystems.Select(es => es.Name).ToList());
|
||||
|
||||
// WP2.6a: drop the shared read cache so the next script call sees
|
||||
// Drop the shared read cache so the next script call sees
|
||||
// the freshly-applied definitions/method lists instead of a stale
|
||||
// snapshot loaded before this deploy.
|
||||
_externalSystemCache?.InvalidateAll();
|
||||
@@ -3335,7 +3334,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// artifact bundle. Reconciled removals ride the same
|
||||
// message so the live DCL connection actor is stopped
|
||||
// on the actor thread alongside the hash-cache eviction.
|
||||
// Sent after the secrets step — see pendingDclApply above (M2).
|
||||
// Sent after the secrets step — see pendingDclApply above.
|
||||
pendingDclApply = new ApplyArtifactDataConnectionsToDcl(
|
||||
command.DataConnections, removedConnections);
|
||||
}
|
||||
@@ -3343,7 +3342,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
// SMTP configuration is
|
||||
// central-only and is never stored on a site (see the purge above).
|
||||
|
||||
// Site-secrets artifact deploy (Task 10): write the shipped secrets to the
|
||||
// Site-secrets artifact deploy: write the shipped secrets to the
|
||||
// replicated deployed_secrets table, then reconcile THIS node's own mirror
|
||||
// synchronously — the reply's SecretsApplied must reflect this node's own
|
||||
// apply/verify outcome, not whatever the next periodic pass finds. A null
|
||||
@@ -3465,7 +3464,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
/// </param>
|
||||
/// <param name="staticStateResetGate">
|
||||
/// The deploy path's static-override / native-alarm reset, dispatched BEFORE this call so the
|
||||
/// new actor's PreStart override load can be ordered after it (arch-review 2026-08-19 P1-2).
|
||||
/// new actor's PreStart override load can be ordered after it.
|
||||
/// <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>
|
||||
@@ -3479,13 +3478,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
//
|
||||
// This used to be the caller's job, and only TWO of the four callers did it: the
|
||||
// boot-time batch and ApplyDeployment. The two self-heal starts did not —
|
||||
// StartStoredInstance (the shared row-67 ensure-started / row-78 sweep path) and
|
||||
// StartStoredInstance (the shared ensure-started / reconcile sweep path) and
|
||||
// HandleEnableResult. Both therefore created an instance whose every tag subscribe was
|
||||
// answered "Unknown connection: X" by the DCL manager, forever, because nothing else on
|
||||
// the node would ever create that connection actor:
|
||||
//
|
||||
// - Row 85's live symptom (2026-08-16): a replication-seeded deployed_configurations row
|
||||
// is started by the row-78 sweep, but the connection actor is only ever created as a
|
||||
// is started by the reconcile sweep, but the connection actor is only ever created as a
|
||||
// side effect of a deploy or the boot load, so tag subscribes failed for 6+ minutes
|
||||
// until a second pair restart let the boot load create it.
|
||||
// - The same defect is reachable with no replication at all: the boot load reads only
|
||||
@@ -3587,7 +3586,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
internal int InstanceActorCount => _instanceActors.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the health collector with current instance counts, and (WP3.1) grows the
|
||||
/// Updates the health collector with current instance counts, and grows the
|
||||
/// script-execution pool to match.
|
||||
/// Total deployed = _deployedInstanceNames.Count, enabled = running actors, disabled = difference.
|
||||
/// </summary>
|
||||
@@ -3598,7 +3597,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
enabled: _instanceActors.Count,
|
||||
disabled: _deployedInstanceNames.Count - _instanceActors.Count);
|
||||
|
||||
// WP3.1: the blocking script pool scales with the number of running instances rather
|
||||
// The blocking script pool scales with the number of running instances rather
|
||||
// than sitting at a fixed 8 forever. This is the single call site because it already
|
||||
// runs on every deploy / undeploy / enable / disable and once per staggered startup
|
||||
// batch. Growth is idempotent and one-way — see ScriptExecutionScheduler.EnsureCapacity.
|
||||
@@ -3672,7 +3671,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
List<DeployedInstance> EnabledConfigs, int CompiledCount, int TotalCount);
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1 warm-then-gate: piped back to self once a deployment's scripts have been
|
||||
/// Warm-then-gate: piped back to self once a deployment's scripts have been
|
||||
/// compiled off the actor thread into the process-wide compile cache. Carries the exact
|
||||
/// command instance it warmed so a result superseded mid-warm is recognised by reference
|
||||
/// and dropped.
|
||||
@@ -3680,7 +3679,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
internal sealed record DeployCompileWarmed(DeployInstanceCommand Command);
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: piped back to self once a staggered startup batch's compiles have been
|
||||
/// Piped back to self once a staggered startup batch's compiles have been
|
||||
/// pre-warmed, carrying the original batch message so actor creation resumes unchanged.
|
||||
/// </summary>
|
||||
internal sealed record BatchCompileWarmed(StartNextBatch Batch);
|
||||
|
||||
@@ -105,11 +105,11 @@ public class InstanceActor : ReceiveActor
|
||||
// deserialized configuration (last-wins on duplicate canonical names,
|
||||
// mirroring the rest of the actor's by-name dictionaries). Each entry
|
||||
// additionally carries its DataType/ElementDataType parsed ONCE at
|
||||
// config-apply time (perf remediation, arch-review WP1.5) so the hot
|
||||
// config-apply time so the hot
|
||||
// TagValueUpdate path never re-Enum.TryParse's a string per update.
|
||||
private readonly Dictionary<string, AttributeIndexEntry> _resolvedAttributeByName = new();
|
||||
|
||||
// Static-override write coalescing (perf remediation, arch-review WP1.5):
|
||||
// Static-override write coalescing:
|
||||
// mirrors NativeAlarmActor's MarkDirtyUpsert/FlushDirtyUpserts (P4) shape.
|
||||
// A burst of rapid SetAttribute calls on the same or different attributes
|
||||
// collapses into one batched SQLite transaction on a short single-shot
|
||||
@@ -124,7 +124,7 @@ public class InstanceActor : ReceiveActor
|
||||
/// <summary>
|
||||
/// The deploy path's static-override / native-alarm reset, supplied by
|
||||
/// <see cref="DeploymentManagerActor.CreateInstanceActor"/> on a (re)deploy and
|
||||
/// <see langword="null"/> on every other start (arch-review 2026-08-19 P1-2). When set,
|
||||
/// <see langword="null"/> on every other start When set,
|
||||
/// <see cref="PreStart"/> starts the override load only once it has completed, so a
|
||||
/// redeploy can never load the PREDECESSOR's overrides into the successor's memory.
|
||||
/// </summary>
|
||||
@@ -135,8 +135,7 @@ public class InstanceActor : ReceiveActor
|
||||
internal sealed record StaticOverridesFlushed(IActorRef Actor, Exception? Error);
|
||||
|
||||
/// <summary>
|
||||
/// Cached string forms of <see cref="QualityCode"/> (perf remediation, arch-review
|
||||
/// WP1.5) — the TagValueUpdate ingest path calls this once or twice per update, so a
|
||||
/// Cached string forms of <see cref="QualityCode"/> — the TagValueUpdate ingest path calls this once or twice per update, so a
|
||||
/// static lookup replaces repeated <see cref="Enum.ToString()"/> reflection.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<QualityCode, string> QualityStrings = new()
|
||||
@@ -150,7 +149,7 @@ public class InstanceActor : ReceiveActor
|
||||
QualityStrings.TryGetValue(quality, out var s) ? s : quality.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// O(1) attribute-resolution index entry (perf remediation, arch-review WP1.5): wraps a
|
||||
/// O(1) attribute-resolution index entry: wraps a
|
||||
/// <see cref="ResolvedAttribute"/> together with its <see cref="DataType"/> /
|
||||
/// element <see cref="DataType"/> / IsList classification, parsed ONCE when the entry
|
||||
/// is built (constructor) instead of re-parsed via <see cref="Enum.TryParse{DataType}"/>
|
||||
@@ -280,7 +279,7 @@ public class InstanceActor : ReceiveActor
|
||||
{
|
||||
// Parse DataType/ElementDataType ONCE here (config-apply time) and index
|
||||
// the wrapping entry for O(1) lookup on the hot TagValueUpdate ingest path
|
||||
// (last-wins on duplicate names) — perf remediation, arch-review WP1.5.
|
||||
// (last-wins on duplicate names)
|
||||
var dataType = ParseDataType(attr.DataType);
|
||||
var elementDataType = ParseElementDataType(attr.ElementDataType);
|
||||
var isList = dataType == DataType.List;
|
||||
@@ -398,8 +397,7 @@ public class InstanceActor : ReceiveActor
|
||||
|
||||
// Asynchronously load static overrides from SQLite and pipe to self. On a (re)deploy
|
||||
// the read is chained behind the reset the Deployment Manager dispatched before this
|
||||
// actor existed, so it can only ever observe the CLEARED table (arch-review
|
||||
// 2026-08-19 P1-2). A faulted reset does not block the load — the deploy itself fails
|
||||
// actor existed, so it can only ever observe the CLEARED table. A faulted reset does not block the load — the deploy itself fails
|
||||
// on that fault through the persistence join, which stops this actor anyway.
|
||||
var self = Self;
|
||||
var instanceName = _instanceUniqueName;
|
||||
@@ -519,7 +517,7 @@ public class InstanceActor : ReceiveActor
|
||||
private void HandleSetStaticAttribute(SetStaticAttributeCommand command)
|
||||
{
|
||||
// O(1) lookup via the existing hot-path index instead of a linear scan over
|
||||
// _configuration.Attributes (perf remediation, arch-review WP1.5).
|
||||
// _configuration.Attributes.
|
||||
_resolvedAttributeByName.TryGetValue(command.AttributeName, out var resolved);
|
||||
|
||||
// Reject writes targeting an attribute that does not exist
|
||||
@@ -640,7 +638,7 @@ public class InstanceActor : ReceiveActor
|
||||
|
||||
/// <summary>
|
||||
/// Records the latest value per attribute name and arms the coalesced flush timer
|
||||
/// (perf remediation, arch-review WP1.5) — mirrors NativeAlarmActor's
|
||||
/// — mirrors NativeAlarmActor's
|
||||
/// MarkDirtyUpsert/FlushDirtyUpserts (P4) shape. A burst of rapid static writes
|
||||
/// collapses into one batched SQLite transaction instead of a write per call.
|
||||
/// </summary>
|
||||
@@ -1157,7 +1155,7 @@ public class InstanceActor : ReceiveActor
|
||||
/// actor.
|
||||
///
|
||||
/// Takes the already-parsed <paramref name="dataType"/>/<paramref name="elementType"/>
|
||||
/// (perf remediation, arch-review WP1.5) rather than re-parsing from the attribute's
|
||||
/// rather than re-parsing from the attribute's
|
||||
/// raw DataType strings — every call site now resolves these once via
|
||||
/// <see cref="AttributeIndexEntry"/> at config-apply time.
|
||||
/// </summary>
|
||||
@@ -1185,8 +1183,7 @@ public class InstanceActor : ReceiveActor
|
||||
/// non-enumerable value, or any element that cannot be coerced — the caller then
|
||||
/// marks the attribute quality Bad. Never throws.
|
||||
///
|
||||
/// Takes the already-parsed <paramref name="elementType"/> (perf remediation,
|
||||
/// arch-review WP1.5) — see <see cref="DecodeAttributeValue"/>.
|
||||
/// Takes the already-parsed <paramref name="elementType"/> — see <see cref="DecodeAttributeValue"/>.
|
||||
/// </summary>
|
||||
private bool TryCoerceListValue(DataType? elementType, object? incoming, out object? typedList)
|
||||
{
|
||||
@@ -1325,13 +1322,18 @@ public class InstanceActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure backoff step for the tag-subscribe retry (Lever 1, 2026-08-28): double the
|
||||
/// Pure backoff step for the tag-subscribe retry: double the
|
||||
/// interval just used, apply ±20 % jitter from <paramref name="jitterUnit"/> (a
|
||||
/// uniform sample in [0,1]; 0.5 = no jitter) so N instances on one connection do not
|
||||
/// re-storm in lockstep, then clamp to [floor, ceiling]. A ceiling below the floor
|
||||
/// degrades to a fixed floor interval rather than shrinking below it — same contract
|
||||
/// as <c>DataConnectionActor.NextTagResolutionInterval</c>.
|
||||
/// </summary>
|
||||
/// <param name="current">The interval just used.</param>
|
||||
/// <param name="floor">The minimum interval.</param>
|
||||
/// <param name="max">The maximum interval; a value below <paramref name="floor"/> degrades to the floor.</param>
|
||||
/// <param name="jitterUnit">A uniform sample in [0,1] used to apply jitter; 0.5 means no jitter.</param>
|
||||
/// <returns>The next retry interval, clamped to the allowed range.</returns>
|
||||
internal static TimeSpan NextTagSubscribeInterval(TimeSpan current, TimeSpan floor, TimeSpan max, double jitterUnit)
|
||||
{
|
||||
var ceiling = max < floor ? floor : max;
|
||||
@@ -1658,7 +1660,7 @@ public class InstanceActor : ReceiveActor
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
// Alarms-only projection (WP2.3): the central live-alarm cache seeds/reconciles
|
||||
// Alarms-only projection: the central live-alarm cache seeds/reconciles
|
||||
// through this same query surface and throws every attribute row away. Building
|
||||
// and shipping them is pure waste — one instance with a few hundred attributes
|
||||
// costs more on the wire than the whole alarm set of the site. The flag is
|
||||
@@ -2153,7 +2155,7 @@ public class InstanceActor : ReceiveActor
|
||||
/// <param name="SourceExecutionId">Execution id of the writer that took the arm.</param>
|
||||
private sealed record DisarmTrace(string AttributeName, Guid SourceExecutionId);
|
||||
|
||||
/// <summary>Self-tell that fires the coalesced static-override persist flush (P4, arch-review WP1.5).</summary>
|
||||
/// <summary>Self-tell that fires the coalesced static-override persist flush.</summary>
|
||||
private sealed class FlushStaticOverrides
|
||||
{
|
||||
public static readonly FlushStaticOverrides Instance = new();
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
|
||||
/// Holds compiled script delegate, manages trigger configuration, and launches script
|
||||
/// runs per invocation. Does not block on run completion.
|
||||
///
|
||||
/// <para>WP3.1: runs are launched directly via <see cref="ScriptRunLauncher"/> rather than
|
||||
/// <para>Runs are launched directly via <see cref="ScriptRunLauncher"/> rather than
|
||||
/// through a short-lived <c>ScriptExecutionActor</c> child (which had no <c>Receive</c>
|
||||
/// handler, no <c>PostStop</c>, and whose <c>IActorRef</c> was never a message target — pure
|
||||
/// per-run actor-cell overhead). Concurrent runs are bounded by
|
||||
@@ -51,19 +51,19 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
private readonly IServiceProvider? _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Script-execution scheduler seam (#18): the process-wide
|
||||
/// Script-execution scheduler seam: the process-wide
|
||||
/// <see cref="ScriptExecutionScheduler"/> when null, or an injected instance so
|
||||
/// this actor's launched script bodies run on a caller-owned pool instead of the
|
||||
/// shared one. Resolved lazily at each use so the null (host) path stays
|
||||
/// byte-for-byte identical to the previous static call.
|
||||
///
|
||||
/// <para>WP3.1: trigger-expression evaluation no longer uses this scheduler — see
|
||||
/// <para>Trigger-expression evaluation no longer uses this scheduler — see
|
||||
/// <see cref="_evalGate"/>.</para>
|
||||
/// </summary>
|
||||
private readonly ScriptExecutionScheduler? _scheduler;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1 (finding #4): the concurrency gate for trigger-expression evaluation, or null
|
||||
/// The concurrency gate for trigger-expression evaluation, or null
|
||||
/// for the process-wide <see cref="TriggerEvalGate.Shared"/>. Evaluations run as plain
|
||||
/// async work on the shared .NET thread pool behind this gate, NOT on
|
||||
/// <see cref="_scheduler"/> — that is what stops an Expression trigger from queueing
|
||||
@@ -72,7 +72,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
private readonly TriggerEvalGate? _evalGate;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: runs launched but not yet completed (queued or executing) for this script.
|
||||
/// Runs launched but not yet completed (queued or executing) for this script.
|
||||
/// Incremented at launch, decremented on <see cref="ScriptExecutionCompleted"/>, which
|
||||
/// every terminal path emits — including the launch-path catch, so the counter cannot
|
||||
/// leak. Touched only on the actor thread.
|
||||
@@ -80,7 +80,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
private int _runsInFlight;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: when the last shed Warning site event was emitted for this script. Sheds are
|
||||
/// When the last shed Warning site event was emitted for this script. Sheds are
|
||||
/// ALWAYS counted on the health collector; the site event is rate-limited to one per
|
||||
/// script per minute so a hot trigger against a saturated cap cannot flood
|
||||
/// <c>site_events</c>.
|
||||
@@ -152,8 +152,8 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
/// <param name="initialAttributes">Initial attribute snapshot used to seed expression trigger evaluation state.</param>
|
||||
/// <param name="healthCollector">Optional health metrics collector.</param>
|
||||
/// <param name="serviceProvider">Optional DI service provider for script execution context services.</param>
|
||||
/// <param name="scheduler">Optional script-execution scheduler override (#18); null uses the process-wide shared scheduler.</param>
|
||||
/// <param name="evalGate">Optional trigger-expression concurrency gate override (WP3.1); null uses the process-wide shared gate.</param>
|
||||
/// <param name="scheduler">Optional script-execution scheduler override; null uses the process-wide shared scheduler.</param>
|
||||
/// <param name="evalGate">Optional trigger-expression concurrency gate override; null uses the process-wide shared gate.</param>
|
||||
public ScriptActor(
|
||||
string scriptName,
|
||||
string instanceName,
|
||||
@@ -246,7 +246,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
_scriptName, _instanceName);
|
||||
}
|
||||
|
||||
// WP3.1: the Stop-on-failure SupervisorStrategy override is gone with the per-run
|
||||
// The Stop-on-failure SupervisorStrategy override is gone with the per-run
|
||||
// ScriptExecutionActor child it supervised. This actor has no children left; a
|
||||
// launch-path throw is caught in SpawnExecution (which replies to the Ask caller and
|
||||
// releases the in-flight slot) so it can never escalate to InstanceActor, which
|
||||
@@ -348,7 +348,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
/// pending, so a storm of changes collapses to the latest snapshot without
|
||||
/// unbounded task fan-out.
|
||||
///
|
||||
/// <para>WP3.1 (finding #4) changed WHERE it runs and WHEN its clock starts. It used
|
||||
/// <para>Trigger evaluation runs off the script pool and its clock starts at enqueue. It used
|
||||
/// to run on the dedicated <see cref="ScriptExecutionScheduler"/> — the same bounded
|
||||
/// pool as blocking script bodies — so N blocked scripts stalled every Expression
|
||||
/// trigger on the node indefinitely. It now runs as plain async work on the shared
|
||||
@@ -556,14 +556,14 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
/// <see cref="SiteRuntimeOptions.MaxConcurrentRunsPerScript"/>; beyond that the newest
|
||||
/// trigger is shed (see <see cref="ShedRun"/>).
|
||||
///
|
||||
/// <para>WP3.1: the run is launched directly on the script-execution scheduler via
|
||||
/// <para>The run is launched directly on the script-execution scheduler via
|
||||
/// <see cref="ScriptRunLauncher"/> — the actor's mailbox stays on the default dispatcher,
|
||||
/// but the script body runs on the bounded set of dedicated threads, so blocking script
|
||||
/// I/O is contained there and cannot starve the shared .NET thread pool. No per-run child
|
||||
/// actor is created.</para>
|
||||
///
|
||||
/// <para>The cap governs NEW work only (<paramref name="callDepth"/> 0 — trigger-driven
|
||||
/// runs and depth-0 Ask calls). A nested <c>CallScript</c> is exempt: the calling run is
|
||||
/// runs and top-level Ask calls). A nested <c>CallScript</c> is exempt: the calling run is
|
||||
/// itself still counted in <see cref="_runsInFlight"/> while it awaits its callee (the
|
||||
/// slot is released only by <see cref="ScriptExecutionCompleted"/>, sent after the body
|
||||
/// returns), and a script calling ITSELF routes back to this same actor — so counting the
|
||||
@@ -625,7 +625,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
parentExecutionId,
|
||||
// Per-script timeout override (null = use global).
|
||||
_executionTimeoutSeconds,
|
||||
// Scheduler seam (#18): thread this actor's scheduler override down so
|
||||
// Scheduler seam: thread this actor's scheduler override down so
|
||||
// launched script bodies share the same pool (null = process-wide shared).
|
||||
_scheduler,
|
||||
// Flow Debugger (design §5.2): trace flag + value-change trigger detail.
|
||||
@@ -634,7 +634,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// WP3.1 supervision parity: the only failure the removed per-run child could
|
||||
// Supervision parity: the only failure the removed per-run child could
|
||||
// surface was a constructor throw (e.g. queueing onto a disposed scheduler),
|
||||
// which the old OneForOneStrategy logged and stopped — leaving an Ask caller to
|
||||
// hang with no reply. Same log shape, same "coordinator unaffected" outcome, plus
|
||||
@@ -652,11 +652,11 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1 shed policy: refuses the incoming run because
|
||||
/// Shed policy: refuses the incoming run because
|
||||
/// <see cref="SiteRuntimeOptions.MaxConcurrentRunsPerScript"/> runs are already in flight.
|
||||
/// The four already queued/running are kept — they are closest to their own deadlines and
|
||||
/// already charged against them — so nothing is ever reordered. A trigger-driven run is
|
||||
/// simply not launched; a depth-0 Ask (an inbound-API route, or a <c>CallScript</c> from
|
||||
/// simply not launched; a top-level Ask (an inbound-API route, or a <c>CallScript</c> from
|
||||
/// an unrelated script's run) gets an explicit error so the caller fails fast rather than
|
||||
/// hanging to its Ask timeout. Nested (<c>callDepth > 0</c>) launches never reach here —
|
||||
/// see <see cref="SpawnExecution"/>.
|
||||
@@ -709,7 +709,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
|
||||
private void HandleExecutionCompleted(ScriptExecutionCompleted msg)
|
||||
{
|
||||
// WP3.1: release the in-flight slot. Every terminal path emits exactly one of these
|
||||
// Release the in-flight slot. Every terminal path emits exactly one of these
|
||||
// (success / timeout / failure / launch failure), so the counter tracks reality.
|
||||
if (_runsInFlight > 0) _runsInFlight--;
|
||||
|
||||
@@ -719,7 +719,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: runs launched but not yet completed. Exposed for regression coverage of the
|
||||
/// Runs launched but not yet completed. Exposed for regression coverage of the
|
||||
/// shed cap — the counter is otherwise invisible from outside the actor.
|
||||
/// </summary>
|
||||
internal int RunsInFlight => _runsInFlight;
|
||||
|
||||
@@ -116,7 +116,7 @@ public class SiteStorageService
|
||||
/// or <see langword="null"/> if no row exists for it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Register P1-4: <see cref="GetAllDeployedConfigsAsync"/> plus a <c>FirstOrDefault</c> was
|
||||
/// <see cref="GetAllDeployedConfigsAsync"/> plus a <c>FirstOrDefault</c> was
|
||||
/// materializing every deployed config — including every <c>ConfigJson</c> blob — just to find
|
||||
/// one row. A volume-restore reconcile healing hundreds of instances turned that into hundreds
|
||||
/// of full-table reads. Callers that need the full row (including <c>ConfigJson</c>) for exactly
|
||||
@@ -162,7 +162,7 @@ public class SiteStorageService
|
||||
/// <c>config_json</c> column, or <see langword="null"/> if no row exists for it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Register P1-4: for a caller that only needs staleness/identity metadata (e.g. the
|
||||
/// For a caller that only needs staleness/identity metadata (e.g. the
|
||||
/// central deployment-state query, which reports only <c>DeploymentId</c>/<c>RevisionHash</c>),
|
||||
/// reading the <c>config_json</c> blob is pure waste. This projection is that lighter read.
|
||||
/// </remarks>
|
||||
@@ -432,8 +432,8 @@ public class SiteStorageService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch counterpart of <see cref="SetStaticOverrideAsync"/> (perf remediation,
|
||||
/// arch-review WP1.5): upserts a set of static attribute overrides for one instance
|
||||
/// Batch counterpart of <see cref="SetStaticOverrideAsync"/> (perf remediation):
|
||||
/// upserts a set of static attribute overrides for one instance
|
||||
/// in a SINGLE connection + transaction with one prepared command re-bound per row.
|
||||
/// Used by the InstanceActor's coalesced flush — mirrors NativeAlarmActor's
|
||||
/// MarkDirtyUpsert/FlushDirtyUpserts (P4) pattern — so a burst of rapid SetAttribute
|
||||
@@ -802,7 +802,7 @@ public class SiteStorageService
|
||||
/// Returns every row in <c>deployed_secrets</c> like <see cref="GetDeployedSecretsAsync"/>,
|
||||
/// but never throws for a single bad row: a row whose <c>row_json</c> does not deserialize
|
||||
/// is returned in <c>Unreadable</c> (name, removal flag and the parse error — never the
|
||||
/// row's content) instead of failing the whole read (final review M4, per-row isolation).
|
||||
/// row's content) instead of failing the whole read (per-row isolation).
|
||||
/// </summary>
|
||||
/// <returns>The parsed rows and the rows that could not be parsed.</returns>
|
||||
public async Task<(IReadOnlyList<DeployedSecretRow> Rows, IReadOnlyList<UnreadableDeployedSecretRow> Unreadable)>
|
||||
@@ -925,7 +925,7 @@ public class SiteStorageService
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
// ── Secret mirror status (review finding M1 — LocalDb-replicated, one row per node) ──
|
||||
// ── Secret mirror status (LocalDb-replicated, one row per node) ──
|
||||
|
||||
/// <summary>
|
||||
/// Records <paramref name="nodeName"/>'s current deployed-secrets mirror failures in the
|
||||
@@ -1418,7 +1418,7 @@ public class DeployedInstance
|
||||
/// Deployment-identity metadata for one deployed instance — the projection of
|
||||
/// <see cref="DeployedInstance"/> that omits <c>ConfigJson</c>. Returned by
|
||||
/// <see cref="SiteStorageService.GetDeployedConfigMetadataAsync"/> for callers that only need to
|
||||
/// answer "is this instance deployed, and at what revision" (register P1-4).
|
||||
/// answer "is this instance deployed, and at what revision".
|
||||
/// </summary>
|
||||
/// <param name="InstanceUniqueName">The unique name of the instance.</param>
|
||||
/// <param name="DeploymentId">The unique deployment identifier.</param>
|
||||
|
||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// WP2.6a (arch-review misc — site external-system resolution): the shared,
|
||||
/// The shared,
|
||||
/// redeploy-invalidated snapshot backing <see cref="SiteExternalSystemRepository"/>'s
|
||||
/// reads.
|
||||
/// <para>
|
||||
@@ -62,6 +62,7 @@ public sealed class ExternalSystemDefinitionCache
|
||||
/// </summary>
|
||||
/// <param name="loader">Loads a fresh snapshot from storage; invoked at most once per miss.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the load.</param>
|
||||
/// <returns>The cached or freshly loaded snapshot.</returns>
|
||||
internal async Task<Snapshot> GetOrLoadAsync(
|
||||
Func<CancellationToken, Task<Snapshot>> loader, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories;
|
||||
/// Write operations throw <see cref="NotSupportedException"/> because site-local
|
||||
/// artifacts are managed exclusively through deployment from Central.
|
||||
/// <para>
|
||||
/// WP2.6a (arch-review misc): every <c>ExternalSystemDefinition</c>/<c>ExternalSystemMethod</c>
|
||||
/// Every <c>ExternalSystemDefinition</c>/<c>ExternalSystemMethod</c>
|
||||
/// read is served from an <see cref="ExternalSystemDefinitionCache"/> snapshot instead of
|
||||
/// a fresh SQLite scan per call — the by-ID lookups in particular used to re-fetch every
|
||||
/// row (and, for methods, re-parse every system's JSON method list) just to find one
|
||||
|
||||
@@ -63,6 +63,9 @@ public sealed class FilesAccessor
|
||||
?? throw new ScriptFileAccessException(ScriptFileAccessFailure.Disabled,
|
||||
"Script file access is not available on this node.");
|
||||
|
||||
/// <summary>Splits text into lines on CRLF, LF or CR boundaries.</summary>
|
||||
/// <param name="text">The text to split.</param>
|
||||
/// <returns>The lines of <paramref name="text"/>; empty when the text is empty.</returns>
|
||||
internal static IReadOnlyList<string> SplitLines(string text)
|
||||
{
|
||||
if (text.Length == 0)
|
||||
|
||||
@@ -112,7 +112,7 @@ public class AttributeAccessor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset-aware handshake (Gitea #36): writes <paramref name="resetKey"/>=<paramref name="resetValue"/>,
|
||||
/// Reset-aware handshake: writes <paramref name="resetKey"/>=<paramref name="resetValue"/>,
|
||||
/// waits (event-driven, bounded by <paramref name="resetTimeout"/>, default = <paramref name="timeout"/>)
|
||||
/// for the device to ECHO that reset, and only then performs the ordinary
|
||||
/// <see cref="WriteBatchAndWaitAsync(IReadOnlyDictionary{string, object?}, string, object?, string, object?, TimeSpan)"/>
|
||||
|
||||
@@ -36,7 +36,7 @@ public enum WorkerDetachOutcome
|
||||
/// of dedicated threads, so script blocking is contained to those threads and cannot
|
||||
/// exhaust the global pool.
|
||||
///
|
||||
/// <para>WP3.1 changed three things about that pool:</para>
|
||||
/// <para>The pool differs from a fixed-size pool in three ways:</para>
|
||||
/// <list type="number">
|
||||
/// <item>It is no longer fixed-size. <see cref="EnsureCapacity"/> grows it towards
|
||||
/// <see cref="ComputeTargetThreads"/> (instance-scaled, clamped between the configured
|
||||
@@ -48,7 +48,7 @@ public enum WorkerDetachOutcome
|
||||
/// <item>Trigger-expression evaluations no longer run here at all — they are non-blocking
|
||||
/// by construction and now run on the shared thread pool behind
|
||||
/// <see cref="TriggerEvalGate"/>, so an alarm's Expression trigger can never queue behind
|
||||
/// eight blocking script bodies (finding #4).</item>
|
||||
/// eight blocking script bodies.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// The scheduler is process-wide (one set of threads for all instances) and is sized
|
||||
|
||||
@@ -157,7 +157,7 @@ public sealed class ScriptLogger
|
||||
|
||||
try
|
||||
{
|
||||
// Final review M3: worded identically to the central InboundScriptLogger's closing row,
|
||||
// Worded identically to the central InboundScriptLogger's closing row,
|
||||
// so the two surfaces read the same in the step-log panel (only the option key differs).
|
||||
_emit("warn", "runtime",
|
||||
$"log line cap reached - {n} further log line(s) were not captured " +
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md):
|
||||
/// Site-events volume policy (docs/plans/2026-08-15-site-events-policy-design.md):
|
||||
/// decides whether an instance script run should emit the legacy per-run Started/Completed
|
||||
/// Info site events, or stay silent and let <see cref="ScriptRunSummaryRecorder"/>'s interval
|
||||
/// aggregate carry the signal instead. Pure and side-effect-free — no DI, no actor context —
|
||||
|
||||
@@ -21,7 +21,7 @@ using ZB.MOM.WW.ScadaBridge.StoreAndForward;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: launches one script or alarm on-trigger run directly from its owning coordinator
|
||||
/// Launches one script or alarm on-trigger run directly from its owning coordinator
|
||||
/// (<see cref="ScriptActor"/> / <see cref="AlarmActor"/>), replacing the short-lived
|
||||
/// <c>ScriptExecutionActor</c> and <c>AlarmExecutionActor</c>.
|
||||
///
|
||||
@@ -76,7 +76,7 @@ internal static class ScriptRunLauncher
|
||||
/// <param name="serviceProvider">Optional DI service provider for script execution services.</param>
|
||||
/// <param name="parentExecutionId">ExecutionId of the spawning execution for audit correlation; null for root runs.</param>
|
||||
/// <param name="executionTimeoutSeconds">Per-script execution timeout in seconds. Null or non-positive falls back to the global value.</param>
|
||||
/// <param name="scheduler">Script-execution scheduler seam (#18); null selects the process-wide shared instance.</param>
|
||||
/// <param name="scheduler">Script-execution scheduler seam; null selects the process-wide shared instance.</param>
|
||||
/// <param name="trace">Flow Debugger trace flag (design 2026-09-05 §5.2): when true the run emits <c>ScriptTrace</c> rows and returns them inline on the <see cref="ScriptCallResult"/>.</param>
|
||||
/// <param name="triggerExtra">Flow Debugger: trigger detail for a traced value-change run (stamped into the <c>ScriptRun</c> row's <c>Extra</c>); null for calls.</param>
|
||||
public static void LaunchScript(
|
||||
@@ -146,13 +146,12 @@ internal static class ScriptRunLauncher
|
||||
/// <param name="completionTarget">The owning <see cref="AlarmActor"/>, which receives the completion message.</param>
|
||||
/// <param name="logger">Logger for execution diagnostics.</param>
|
||||
/// <param name="healthCollector">
|
||||
/// Optional health collector for recording execution metrics (arch-review 2026-08-19
|
||||
/// P1-3): the alarm on-trigger run shares the script-error counter with
|
||||
/// Optional health collector for recording execution metrics: the alarm on-trigger run shares the script-error counter with
|
||||
/// <see cref="RunScriptAsync"/> so a wedged/failed alarm body is visible the same way a
|
||||
/// wedged/failed instance script is.
|
||||
/// </param>
|
||||
/// <param name="serviceProvider">
|
||||
/// Optional DI service provider (arch-review 2026-08-19 P1-3): resolves the singleton
|
||||
/// Optional DI service provider: resolves the singleton
|
||||
/// <see cref="ISiteEventLogger"/> (stuck-watchdog reporting) and
|
||||
/// <see cref="ScriptRunSummaryRecorder"/> (aggregate run counters), exactly like the
|
||||
/// instance-script path.
|
||||
@@ -164,7 +163,7 @@ internal static class ScriptRunLauncher
|
||||
/// write fired this alarm. Null when the firing value came from the Data Connection Layer
|
||||
/// (external data has no spawning execution) — that on-trigger run is a tree ROOT.
|
||||
/// </param>
|
||||
/// <param name="scheduler">Script-execution scheduler seam (#18); null selects the process-wide shared instance.</param>
|
||||
/// <param name="scheduler">Script-execution scheduler seam; null selects the process-wide shared instance.</param>
|
||||
/// <param name="siteCommunicationActor">Site transport for notification status queries, resolved on the actor thread.</param>
|
||||
public static void LaunchAlarmScript(
|
||||
string alarmName,
|
||||
@@ -268,7 +267,7 @@ internal static class ScriptRunLauncher
|
||||
// it is available to the catch blocks regardless of scope state.
|
||||
var siteEventLogger = serviceProvider?.GetService<ISiteEventLogger>();
|
||||
|
||||
// WP3.2 (site_events volume policy): both are singletons, resolved from the root
|
||||
// site_events volume policy: both are singletons, resolved from the root
|
||||
// provider up front so they too are available to the catch blocks. optionsMonitor is
|
||||
// null in tests that pass no serviceProvider (SingleServiceProvider et al.) — the
|
||||
// constructor-passed `options` is the fallback, exactly like siteEventLogger's null
|
||||
@@ -282,7 +281,7 @@ internal static class ScriptRunLauncher
|
||||
var emitPerRunEvents = ScriptRunEventPolicy.ShouldEmitPerRun(effectiveOptions, instanceName, scriptName);
|
||||
var runStopwatch = Stopwatch.StartNew();
|
||||
|
||||
// WP3.1: identify the worker this body is holding, so the watchdog can detach and
|
||||
// Identify the worker this body is holding, so the watchdog can detach and
|
||||
// replace it if the body wedges. Read in the FIRST synchronous segment — a script
|
||||
// that later hops threads on an await no longer holds this worker, and the run-stamp
|
||||
// guard in TryDetachWorker will (correctly) refuse to detach it.
|
||||
@@ -299,7 +298,7 @@ internal static class ScriptRunLauncher
|
||||
|
||||
try
|
||||
{
|
||||
// WP3.1 shed-at-dequeue: the deadline was armed at ENQUEUE, so an already-cancelled
|
||||
// Shed-at-dequeue: the deadline was armed at ENQUEUE, so an already-cancelled
|
||||
// token here means this run spent its entire budget queueing. Running it now would
|
||||
// burn a scarce script thread on work that is already stale — throw straight into
|
||||
// the existing timeout path instead, BEFORE the "started" event and before the body.
|
||||
@@ -421,7 +420,7 @@ internal static class ScriptRunLauncher
|
||||
Scope = scope
|
||||
};
|
||||
|
||||
// WP3.2: the aggregate ALWAYS records this run (that's the point of the interval
|
||||
// The aggregate ALWAYS records this run (that's the point of the interval
|
||||
// summary), but the per-run Info event itself is gated by ScriptRunEventPolicy —
|
||||
// off by default, on globally via PerRunScriptEvents, or on for named scripts via
|
||||
// PerRunScriptEventScripts. Fire-and-forget (the `_ =` discards the task) so the
|
||||
@@ -478,7 +477,7 @@ internal static class ScriptRunLauncher
|
||||
var errorMsg = $"Script '{scriptName}' on instance '{instanceName}' timed out after {timeout.TotalSeconds}s";
|
||||
logger.LogWarning("{Message} (run {RunId})", errorMsg, runId);
|
||||
|
||||
// Failures recorded to site event log; script NOT disabled after failure. WP3.2:
|
||||
// Failures recorded to site event log; script NOT disabled after failure.
|
||||
// Error-level emissions are unconditional — never gated by ScriptRunEventPolicy.
|
||||
_ = siteEventLogger?.LogEventAsync(
|
||||
"script", "Error", instanceName, $"ScriptActor:{scriptName}", errorMsg);
|
||||
@@ -504,7 +503,7 @@ internal static class ScriptRunLauncher
|
||||
{
|
||||
healthCollector?.IncrementScriptError();
|
||||
summaryRecorder?.RecordFailed(instanceName, scriptName, runStopwatch.ElapsedMilliseconds);
|
||||
// Failures recorded to site event log; script NOT disabled after failure. WP3.2:
|
||||
// Failures recorded to site event log; script NOT disabled after failure.
|
||||
// Error-level emissions are unconditional — never gated by ScriptRunEventPolicy.
|
||||
var errorMsg = $"Script '{scriptName}' on instance '{instanceName}' failed: {ex.Message}";
|
||||
logger.LogError(ex, "Script execution failed: {Script} on {Instance} (run {RunId})",
|
||||
@@ -556,6 +555,13 @@ internal static class ScriptRunLauncher
|
||||
/// the ACTOR thread, outside any launch scope. <c>elapsedMs</c> is 0 — the run never ran.
|
||||
/// Best-effort and never throws: a no-op when there is no provider or no writer.</para>
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">Root service provider used to resolve the audit writer and identity providers; no-op when <c>null</c>.</param>
|
||||
/// <param name="logger">Logger for best-effort failure reporting.</param>
|
||||
/// <param name="scriptName">Name of the script whose run was refused.</param>
|
||||
/// <param name="instanceName">Name of the instance owning the script.</param>
|
||||
/// <param name="parentExecutionId">ExecutionId of the spawning execution; null for root runs.</param>
|
||||
/// <param name="reason">Human-readable guard reason the run was refused.</param>
|
||||
/// <param name="triggerExtra">Trigger detail stamped into the row's <c>Extra</c> for a traced value-change run; null otherwise.</param>
|
||||
internal static void EmitSuppressedRun(
|
||||
IServiceProvider? serviceProvider,
|
||||
ILogger logger,
|
||||
@@ -668,7 +674,7 @@ internal static class ScriptRunLauncher
|
||||
CancellationTokenSource cts,
|
||||
ICanTell? siteCommunicationActor)
|
||||
{
|
||||
// arch-review 2026-08-19 P1-3: singletons resolved from the root provider up front,
|
||||
// Singletons resolved from the root provider up front,
|
||||
// exactly like RunScriptAsync, so they are available to the catch blocks regardless
|
||||
// of scope state. Null in tests that pass no serviceProvider — degrades the same way
|
||||
// the instance-script path does (no site event log / no aggregate counters).
|
||||
@@ -681,7 +687,7 @@ internal static class ScriptRunLauncher
|
||||
var scriptName = $"AlarmActor:{alarmName}";
|
||||
var runStopwatch = Stopwatch.StartNew();
|
||||
|
||||
// arch-review 2026-08-19 P1-3: identify the worker this body is holding, so the
|
||||
// Identify the worker this body is holding, so the
|
||||
// stuck-script watchdog can detach and replace it if the body wedges — same
|
||||
// mechanism, same gauge, as the instance-script path. Read in the FIRST synchronous
|
||||
// segment, before any await.
|
||||
@@ -716,7 +722,7 @@ internal static class ScriptRunLauncher
|
||||
timeout,
|
||||
instanceName,
|
||||
logger,
|
||||
// Final review M6: name the source so ScriptRuntimeContext's `_logTarget` derives
|
||||
// Name the source so ScriptRuntimeContext's `_logTarget` derives
|
||||
// the alarm this on-trigger run belongs to ("AlarmActor:Foo" -> "Foo") — without
|
||||
// it every Log.* line from an alarm script was attributed to the INSTANCE, not the
|
||||
// alarm. `scriptName` above is already the "AlarmActor:{alarmName}" source key, so
|
||||
@@ -795,7 +801,7 @@ internal static class ScriptRunLauncher
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Arms the stuck-script watchdog (S2, extended by WP3.1).
|
||||
/// Arms the stuck-script watchdog, which can also detach and replace a wedged worker.
|
||||
///
|
||||
/// <para>The CTS firing only REQUESTS cooperative cancellation; it does NOT free a thread
|
||||
/// blocked in synchronous I/O. When the deadline elapses, this waits
|
||||
|
||||
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md):
|
||||
/// Site-events volume policy (docs/plans/2026-08-15-site-events-policy-design.md):
|
||||
/// site-only hosted service that periodically flushes <see cref="ScriptRunSummaryRecorder"/>
|
||||
/// into one aggregate "script" Info site event. Registered by
|
||||
/// <c>ServiceCollectionExtensions.AddSiteRuntime</c>, which only the site composition root
|
||||
|
||||
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md):
|
||||
/// Site_events volume policy (docs/plans/2026-08-15-site-events-policy-design.md):
|
||||
/// accumulates per-(instance, script) run counters between flushes and emits ONE aggregate
|
||||
/// "script" Info site event per interval instead of the two per-run Started/Completed rows
|
||||
/// that were the dominant <c>site_events</c> writer. Registered as a process-wide singleton
|
||||
@@ -54,6 +54,8 @@ public sealed class ScriptRunSummaryRecorder
|
||||
Volatile.Read(ref _counters).GetOrAdd((instanceName, scriptName), static _ => new Counters());
|
||||
|
||||
/// <summary>Records that a script run began executing (past the shed-at-dequeue check).</summary>
|
||||
/// <param name="instanceName">The instance the script belongs to.</param>
|
||||
/// <param name="scriptName">The script that ran.</param>
|
||||
public void RecordStarted(string instanceName, string scriptName)
|
||||
{
|
||||
var counters = GetOrAddCounters(instanceName, scriptName);
|
||||
@@ -61,6 +63,9 @@ public sealed class ScriptRunSummaryRecorder
|
||||
}
|
||||
|
||||
/// <summary>Records that a script run completed successfully.</summary>
|
||||
/// <param name="instanceName">The instance the script belongs to.</param>
|
||||
/// <param name="scriptName">The script that ran.</param>
|
||||
/// <param name="durationMs">The run duration in milliseconds.</param>
|
||||
public void RecordCompleted(string instanceName, string scriptName, long durationMs)
|
||||
{
|
||||
var counters = GetOrAddCounters(instanceName, scriptName);
|
||||
@@ -69,6 +74,9 @@ public sealed class ScriptRunSummaryRecorder
|
||||
}
|
||||
|
||||
/// <summary>Records that a script run threw an unhandled exception.</summary>
|
||||
/// <param name="instanceName">The instance the script belongs to.</param>
|
||||
/// <param name="scriptName">The script that ran.</param>
|
||||
/// <param name="durationMs">The run duration in milliseconds.</param>
|
||||
public void RecordFailed(string instanceName, string scriptName, long durationMs)
|
||||
{
|
||||
var counters = GetOrAddCounters(instanceName, scriptName);
|
||||
@@ -80,6 +88,9 @@ public sealed class ScriptRunSummaryRecorder
|
||||
/// Records that a script run timed out — including a run shed at dequeue (its deadline
|
||||
/// was already spent queueing, so it never reached <see cref="RecordStarted"/>).
|
||||
/// </summary>
|
||||
/// <param name="instanceName">The instance the script belongs to.</param>
|
||||
/// <param name="scriptName">The script that timed out.</param>
|
||||
/// <param name="durationMs">The run duration in milliseconds.</param>
|
||||
public void RecordTimedOut(string instanceName, string scriptName, long durationMs)
|
||||
{
|
||||
var counters = GetOrAddCounters(instanceName, scriptName);
|
||||
@@ -202,7 +213,7 @@ public sealed class ScriptRunSummaryRecorder
|
||||
return true;
|
||||
}
|
||||
|
||||
// Built once per assembly (WP1.5 convention) rather than per flush.
|
||||
// Built once per assembly rather than per flush.
|
||||
private static readonly JsonSerializerOptions DetailsJsonOptions =
|
||||
new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
|
||||
|
||||
@@ -310,6 +310,7 @@ public class ScriptRuntimeContext
|
||||
/// <see cref="CreateChildContextForSharedScript"/>, so an inline shared script shares its
|
||||
/// caller's line cap and anchor. Null (the default) creates this run's own logger.
|
||||
/// </param>
|
||||
/// <param name="fileReader">Host-mediated reader backing the script <c>Files</c> global; null disables file access.</param>
|
||||
public ScriptRuntimeContext(
|
||||
IActorRef instanceActor,
|
||||
SharedScriptLibrary sharedScriptLibrary,
|
||||
@@ -422,6 +423,9 @@ public class ScriptRuntimeContext
|
||||
/// Wraps a trace sink so a throwing observer can neither fail the script call nor skip the
|
||||
/// durable audit write that follows it. Null in, null out — an untraced run allocates nothing.
|
||||
/// </summary>
|
||||
/// <param name="sink">The trace sink to wrap, or null for an untraced run.</param>
|
||||
/// <param name="logger">Logger used to report a rejecting sink.</param>
|
||||
/// <returns>The guarded sink, or null when <paramref name="sink"/> is null.</returns>
|
||||
internal static Action<AuditEvent>? GuardTraceSink(Action<AuditEvent>? sink, ILogger logger)
|
||||
{
|
||||
if (sink is null) return null;
|
||||
@@ -973,7 +977,7 @@ public class ScriptRuntimeContext
|
||||
.ConfigureAwait(false)).Matched;
|
||||
|
||||
/// <summary>
|
||||
/// Reset-aware handshake (Gitea #36, design 2026-09-10 §5) backing the nine-argument
|
||||
/// Reset-aware handshake (design 2026-09-10 §5) backing the nine-argument
|
||||
/// <c>Attributes.WriteBatchAndWaitAsync</c> overload. Legs, in a FIXED order:
|
||||
/// (1) write <paramref name="resetAttr"/>=<paramref name="resetEncoded"/> through
|
||||
/// <see cref="SetAttribute"/> (throws on a device-write failure, like the batch leg);
|
||||
@@ -986,7 +990,7 @@ public class ScriptRuntimeContext
|
||||
/// Why the reset must be OBSERVED before the trigger is written: a subscription reports a
|
||||
/// change from the last reported value, so a device that flips reset→done inside one
|
||||
/// sampling interval never reports the reset — the cached previous-run "done" value would
|
||||
/// then satisfy the response wait instantly (the #36 race), and an edge-triggered wait
|
||||
/// then satisfy the response wait instantly (the stale-echo race), and an edge-triggered wait
|
||||
/// would never resolve at all. Holding the trigger until the echo lands makes the existing
|
||||
/// level-triggered fast-path correct again. Each leg is already a traced Flow Debugger
|
||||
/// step, so a traced run shows all four.
|
||||
@@ -1006,7 +1010,7 @@ public class ScriptRuntimeContext
|
||||
/// <param name="resetAttr">Scope-resolved attribute written FIRST (usually the response attribute).</param>
|
||||
/// <param name="resetEncoded">
|
||||
/// Codec-encoded reset value the device must echo before anything else is written. NON-NULL by
|
||||
/// contract: a null would write an empty string on leg 1 and then give the leg-2 waiter a null
|
||||
/// contract: a null would write an empty string on leg 1 and then give the second-leg waiter a null
|
||||
/// target — "any change" — which fast-paths on the stale cached value and reinstates the very
|
||||
/// race this overload exists to close. The accessor rejects a null reset value before it gets here.
|
||||
/// </param>
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class ScriptSchedulerStatsReporter : BackgroundService
|
||||
private readonly TimeSpan _pollInterval;
|
||||
|
||||
/// <summary>
|
||||
/// Script-execution scheduler seam (#18): the process-wide scheduler when null, or
|
||||
/// Script-execution scheduler seam: the process-wide scheduler when null, or
|
||||
/// an injected instance so the reporter samples the same pool the actors run on.
|
||||
/// </summary>
|
||||
private readonly ScriptExecutionScheduler? _scheduler;
|
||||
@@ -35,7 +35,7 @@ public sealed class ScriptSchedulerStatsReporter : BackgroundService
|
||||
/// <param name="options">Site runtime options supplying the shared scheduler's thread count.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="pollInterval">Poll interval override; defaults to <see cref="DefaultPollInterval"/> (10 s).</param>
|
||||
/// <param name="scheduler">Optional script-execution scheduler override (#18); null samples the process-wide shared scheduler.</param>
|
||||
/// <param name="scheduler">Optional script-execution scheduler override; null samples the process-wide shared scheduler.</param>
|
||||
public ScriptSchedulerStatsReporter(
|
||||
ISiteHealthCollector collector,
|
||||
SiteRuntimeOptions options,
|
||||
@@ -81,7 +81,7 @@ public sealed class ScriptSchedulerStatsReporter : BackgroundService
|
||||
scheduler.QueueDepth,
|
||||
scheduler.BusyThreadCount,
|
||||
scheduler.OldestBusyAge?.TotalSeconds,
|
||||
// WP3.1: workers detached and replaced by the stuck-script watchdog.
|
||||
// Workers detached and replaced by the stuck-script watchdog.
|
||||
scheduler.DetachedThreadCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
/// 4096 because entries pin compiled assemblies, not just verdict strings.
|
||||
/// </para>
|
||||
///
|
||||
/// <para><b>WP3.1 — approximate LRU replaced the overflow cliff.</b> The cache used to
|
||||
/// <para><b>Approximate LRU replaced the overflow cliff.</b> The cache used to
|
||||
/// clear itself wholesale on overflow. Instance scripts AND trigger expressions share this
|
||||
/// cache, so on a site large enough to cross 1024 entries every overflow discarded up to 1023
|
||||
/// live compiled scripts and the next deploy or Instance-Actor start paid a full recompile
|
||||
|
||||
@@ -41,7 +41,10 @@ public sealed class TraceCapturingLogger : ILogger
|
||||
_sink = sink;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Begins a logical scope on the wrapped logger.</summary>
|
||||
/// <typeparam name="TState">The type of the scope state.</typeparam>
|
||||
/// <param name="state">The scope state.</param>
|
||||
/// <returns>A disposable that ends the scope, or null if the inner logger returns none.</returns>
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => _inner.BeginScope(state);
|
||||
|
||||
/// <summary>
|
||||
@@ -52,7 +55,13 @@ public sealed class TraceCapturingLogger : ILogger
|
||||
/// <returns><see langword="true"/> when the entry should be produced.</returns>
|
||||
public bool IsEnabled(LogLevel level) => level >= LogLevel.Information || _inner.IsEnabled(level);
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Forwards the entry to the wrapped logger and captures it into the trace sink.</summary>
|
||||
/// <typeparam name="TState">The type of the entry state.</typeparam>
|
||||
/// <param name="level">The entry's log level.</param>
|
||||
/// <param name="eventId">The entry's event id.</param>
|
||||
/// <param name="state">The entry state.</param>
|
||||
/// <param name="exception">The associated exception, if any.</param>
|
||||
/// <param name="formatter">Formats the state and exception into the message.</param>
|
||||
public void Log<TState>(LogLevel level, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
// Forward first, and only if the inner logger actually wants it: IsEnabled above lies
|
||||
|
||||
@@ -38,7 +38,7 @@ public static class TraceStepScope
|
||||
/// <summary>Restores the ambient step id captured at <see cref="Enter"/> time.</summary>
|
||||
private sealed class Restore(Guid? prev) : IDisposable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
/// <summary>Restores the previous ambient step id.</summary>
|
||||
public void Dispose() => _current.Value = prev;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1 (finding #4): the process-wide concurrency gate for trigger-expression evaluation.
|
||||
/// The process-wide concurrency gate for trigger-expression evaluation.
|
||||
///
|
||||
/// <para>Before WP3.1, <c>ScriptActor</c> and <c>AlarmActor</c> evaluated their compiled
|
||||
/// <para>Previously, <c>ScriptActor</c> and <c>AlarmActor</c> evaluated their compiled
|
||||
/// Expression triggers on the <see cref="ScriptExecutionScheduler"/> — the same bounded pool
|
||||
/// of dedicated threads that runs script bodies. Eight scripts blocked in synchronous I/O
|
||||
/// therefore stalled EVERY Expression trigger on the node behind them, for an unbounded time,
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Secrets;
|
||||
/// <param name="FailedNames">
|
||||
/// The secret name behind each <paramref name="Failed"/> entry, in the SAME order (index-for-
|
||||
/// index — each pass reports at most one failure per row, so the two lists always stay in
|
||||
/// lockstep). Added for <c>DeploymentManagerActor.HandleDeployArtifacts</c> (Task 10): this
|
||||
/// lockstep). Added for <c>DeploymentManagerActor.HandleDeployArtifacts</c>: this
|
||||
/// mirror reconciles the WHOLE site-wide <c>deployed_secrets</c> table every pass, not only the
|
||||
/// names a particular deploy just shipped, so a caller that needs to know whether a failure
|
||||
/// belongs to ITS OWN shipped names — as opposed to some other, previously-deployed secret, or a
|
||||
@@ -54,7 +54,7 @@ public sealed record MirrorResult(
|
||||
/// master key.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Apply semantics and the confirm-then-ledger fix (fix round 1, review Important #1).</b>
|
||||
/// <b>Apply semantics and the confirm-then-ledger fix.</b>
|
||||
/// <see cref="ISecretStore.ApplyReplicatedAsync"/> applies last-writer-wins strictly on
|
||||
/// (<c>UpdatedUtc</c>, <c>Revision</c>) — see
|
||||
/// <see cref="ZB.MOM.WW.Secrets.Abstractions.SecretLastWriterWins"/> — and a row that is not
|
||||
@@ -106,7 +106,7 @@ public sealed record MirrorResult(
|
||||
/// a later pass's "is this ledgered at the shipped revision" check stays correct.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Per-row isolation (review Important #2).</b> Each row's apply/verify body runs in its own
|
||||
/// <b>Per-row isolation.</b> Each row's apply/verify body runs in its own
|
||||
/// try/catch; an exception — an unrecognised <c>ContentType</c> from a newer central, a
|
||||
/// per-row store fault, a malformed name — is caught, reported as a <see cref="MirrorResult.Failed"/>
|
||||
/// entry naming the secret and the exception type/message (never a value), logged with the same
|
||||
@@ -114,7 +114,7 @@ public sealed record MirrorResult(
|
||||
/// caller's own cancellation (<c>ct.IsCancellationRequested</c>) propagates.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Cache invalidation (review Important #3).</b> The concrete resolver
|
||||
/// <b>Cache invalidation.</b> The concrete resolver
|
||||
/// (<c>DefaultSecretResolver</c>) caches decrypted plaintext for a TTL and also implements
|
||||
/// <see cref="ISecretCacheInvalidator"/>; every ZB.MOM.WW.Secrets write path invalidates after a
|
||||
/// write for exactly this reason. This mirror does the same: after every successful apply
|
||||
@@ -124,10 +124,10 @@ public sealed record MirrorResult(
|
||||
/// can observe a stale pre-rotation plaintext for up to the cache TTL.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Node-local store assumption (review Minor #5; Task 11 removed every replication mode).</b>
|
||||
/// <b>Node-local store assumption (every replication mode has been removed).</b>
|
||||
/// This mirror assumes the site's <see cref="ISecretStore"/> is always plain, node-local, local
|
||||
/// SQLite — the ONLY shape a Site node's store can be after Task 11 retired both the pull-only
|
||||
/// gRPC secrets hub and the SQL-Server <c>ReplicatingSecretStore</c> mode that preceded it (see
|
||||
/// SQLite — the ONLY shape a Site node's store can be now that both the pull-only
|
||||
/// gRPC secrets hub and the SQL-Server <c>ReplicatingSecretStore</c> mode that preceded it are retired (see
|
||||
/// <c>SecretsRegistration</c>). It would NOT have been safe to run this mirror against a store
|
||||
/// decorated by either retired replication mode: <c>DeleteAsync</c> would have published the
|
||||
/// tombstone to every other node sharing the decorator, so a site merely un-referencing a secret
|
||||
@@ -156,7 +156,7 @@ public sealed record MirrorResult(
|
||||
/// is in-memory only (per node process), which matches the ledger's own node-local scope.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Status row for central (review finding M1).</b> A failure on the STANDBY node used to be
|
||||
/// <b>Status row for central.</b> A failure on the STANDBY node used to be
|
||||
/// visible only in that node's own log — site health reports are sent by the active node only.
|
||||
/// After each pass this mirror records its node's current failure set (secret name + message,
|
||||
/// never a value) in the LocalDb-replicated <c>secret_mirror_status</c> table, keyed by node
|
||||
@@ -190,7 +190,7 @@ public sealed class DeployedSecretsMirror : BackgroundService
|
||||
/// <summary>
|
||||
/// The failure set (sorted name+message pairs) this process last wrote successfully to
|
||||
/// <c>secret_mirror_status</c>, or <see langword="null"/> before the first successful write
|
||||
/// — which is what forces the once-per-start-up write (review finding M1).
|
||||
/// — which is what forces the once-per-start-up write.
|
||||
/// </summary>
|
||||
private IReadOnlyList<SecretMirrorFailure>? _lastWrittenStatus;
|
||||
|
||||
@@ -273,7 +273,7 @@ public sealed class DeployedSecretsMirror : BackgroundService
|
||||
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Tolerant read (final review M4): a row whose row_json cannot be parsed is handed
|
||||
// Tolerant read: a row whose row_json cannot be parsed is handed
|
||||
// back separately instead of throwing out of the whole pass, so it is reported
|
||||
// against its own name like any other per-row failure.
|
||||
var (rows, unreadable) = await _storage.GetDeployedSecretsTolerantAsync().ConfigureAwait(false);
|
||||
@@ -372,7 +372,7 @@ public sealed class DeployedSecretsMirror : BackgroundService
|
||||
/// or the stored row no longer holds what this process wrote (deleted or overwritten by a
|
||||
/// replicated change).
|
||||
/// Best-effort: never throws, so the pass result is unaffected by a status-write fault.
|
||||
/// See the class remarks (review finding M1).
|
||||
/// See the class remarks.
|
||||
/// </summary>
|
||||
private async Task RecordStatusIfChangedAsync(IReadOnlyList<string> failedNames, IReadOnlyList<string> failed)
|
||||
{
|
||||
@@ -672,7 +672,7 @@ public sealed class DeployedSecretsMirror : BackgroundService
|
||||
{
|
||||
// Covers every other failure, including an OperationCanceledException NOT
|
||||
// attributable to this service's own shutdown token (e.g. a store/command
|
||||
// timeout) — that must not silently end the loop (review Minor #4).
|
||||
// timeout) — that must not silently end the loop.
|
||||
_logger.LogWarning(ex, "Deployed secrets mirror pass failed; retrying next interval.");
|
||||
}
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false));
|
||||
|
||||
@@ -7,7 +7,7 @@ using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Secrets;
|
||||
|
||||
/// <summary>
|
||||
/// Review finding M1: site-side hosted service that periodically reads EVERY node's row from
|
||||
/// Site-side hosted service that periodically reads EVERY node's row from
|
||||
/// the LocalDb-replicated <c>secret_mirror_status</c> table and pushes the list into
|
||||
/// <see cref="ISiteHealthCollector.SetSecretMirrorStatuses"/>, so the next health report
|
||||
/// carries each site node's deployed-secrets mirror failures — the standby's included.
|
||||
|
||||
@@ -40,7 +40,7 @@ public static class ServiceCollectionExtensions
|
||||
// SiteScriptCompileCache, so "one Roslyn compile per script body per process"
|
||||
// (N4) is exactly the statement that there is one of these per host. Downgrading
|
||||
// it to Scoped/Transient would silently give every consumer its own cache and
|
||||
// reintroduce the recompile storms WP3.1 removed — no test would fail on
|
||||
// reintroduce the recompile storms that were removed — no test would fail on
|
||||
// behaviour, only the registration pin in SiteRuntimeServiceRegistrationTests.
|
||||
services.AddSingleton<ScriptCompilationService>();
|
||||
|
||||
@@ -81,7 +81,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<Scripts.IScriptFileReader, Scripts.ScriptFileReader>();
|
||||
|
||||
// Site-local repository implementations backed by SQLite.
|
||||
// WP2.6a: ExternalSystemDefinitionCache is a singleton shared by every scoped
|
||||
// ExternalSystemDefinitionCache is a singleton shared by every scoped
|
||||
// SiteExternalSystemRepository instance (one per script-execution DI scope), so
|
||||
// resolving the repository repeatedly does not each pay a fresh SQLite
|
||||
// fetch-all — DeploymentManagerActor invalidates the shared cache after an
|
||||
@@ -106,14 +106,14 @@ public static class ServiceCollectionExtensions
|
||||
sp.GetRequiredService<IOptions<SiteRuntimeOptions>>().Value,
|
||||
sp.GetRequiredService<ILogger<ScriptSchedulerStatsReporter>>()));
|
||||
|
||||
// WP2.6d: periodically lift SiteStreamManager's alarm-publish-queue drop count
|
||||
// Periodically lift SiteStreamManager's alarm-publish-queue drop count
|
||||
// onto the site health report.
|
||||
services.AddHostedService(sp => new Streaming.SiteStreamAlarmDropReporter(
|
||||
sp.GetRequiredService<HealthMonitoring.ISiteHealthCollector>(),
|
||||
sp.GetRequiredService<SiteStreamManager>(),
|
||||
sp.GetRequiredService<ILogger<Streaming.SiteStreamAlarmDropReporter>>()));
|
||||
|
||||
// WP3.2 (site_events volume policy): the recorder is a singleton so ScriptRunLauncher
|
||||
// Site-events volume policy: the recorder is a singleton so ScriptRunLauncher
|
||||
// can resolve it from the root service provider on every run, the same way it already
|
||||
// resolves ISiteEventLogger. The flush service is registered ONLY here — central
|
||||
// composition roots never call AddSiteRuntime(), which is what makes the periodic
|
||||
@@ -125,7 +125,7 @@ public static class ServiceCollectionExtensions
|
||||
sp.GetRequiredService<IOptionsMonitor<SiteRuntimeOptions>>(),
|
||||
sp.GetRequiredService<ILogger<ScriptRunSummaryFlushService>>()));
|
||||
|
||||
// Site-secrets artifact deploy (Task 9): per-node reconcile of the site-wide,
|
||||
// Site-secrets artifact deploy: per-node reconcile of the site-wide,
|
||||
// LocalDb-replicated deployed_secrets table into THIS node's own ZB.MOM.WW.Secrets
|
||||
// store. Runs on every site node — not a singleton, since mirrored_secrets (which
|
||||
// names this node has already applied) is deliberately node-local, not replicated.
|
||||
@@ -140,7 +140,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<DeployedSecretsMirror>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<DeployedSecretsMirror>());
|
||||
|
||||
// Review finding M1: forward every node's replicated secret_mirror_status row onto the
|
||||
// Forward every node's replicated secret_mirror_status row onto the
|
||||
// site health report, so the ACTIVE node's report carries the STANDBY's mirror failures.
|
||||
services.AddHostedService(sp => new SecretMirrorStatusReporter(
|
||||
sp.GetRequiredService<HealthMonitoring.ISiteHealthCollector>(),
|
||||
|
||||
@@ -42,7 +42,7 @@ public class SiteRuntimeOptions
|
||||
/// Script and alarm on-trigger bodies run on these threads instead of the shared
|
||||
/// .NET thread pool, so blocking script I/O cannot starve the global pool.
|
||||
///
|
||||
/// <para>WP3.1: this was a fixed size and is now the lower bound of a grow-only,
|
||||
/// <para>This was a fixed size and is now the lower bound of a grow-only,
|
||||
/// instance-scaled pool — see
|
||||
/// <see cref="Scripts.ScriptExecutionScheduler.ComputeTargetThreads"/>. Existing
|
||||
/// configurations keep exactly the previous behaviour at or below
|
||||
@@ -53,7 +53,7 @@ public class SiteRuntimeOptions
|
||||
public int ScriptExecutionThreadCount { get; set; } = 8;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: CEILING for the instance-scaled script-execution pool. The pool grows
|
||||
/// CEILING for the instance-scaled script-execution pool. The pool grows
|
||||
/// towards <c>ceil(enabledInstances / 8)</c> threads but never past this value; beyond
|
||||
/// it, <see cref="MaxConcurrentRunsPerScript"/> is the real regulator. A 1 MB-stack
|
||||
/// dedicated thread is cheap, so 32 (≈ 256 instances at the /8 ratio) is a generous
|
||||
@@ -63,7 +63,7 @@ public class SiteRuntimeOptions
|
||||
public int ScriptExecutionMaxThreadCount { get; set; } = 32;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1 (finding #4): maximum number of trigger-expression evaluations allowed to run
|
||||
/// Maximum number of trigger-expression evaluations allowed to run
|
||||
/// concurrently across the whole process. Trigger expressions are non-blocking by
|
||||
/// construction (<see cref="Scripts.TriggerExpressionGlobals"/> exposes only reads over an
|
||||
/// in-memory snapshot, and the script trust gate has already denied I/O, network, and
|
||||
@@ -75,7 +75,7 @@ public class SiteRuntimeOptions
|
||||
public int TriggerEvalMaxConcurrency { get; set; } = Math.Max(2, Environment.ProcessorCount);
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: timeout (seconds) for a single trigger-expression evaluation. Previously
|
||||
/// Timeout (seconds) for a single trigger-expression evaluation. Previously
|
||||
/// hardcoded at 2 s in both <c>ScriptActor</c> and <c>AlarmActor</c>. The deadline is now
|
||||
/// armed when the evaluation is ENQUEUED, not when it is dequeued, so time spent waiting
|
||||
/// on <see cref="TriggerEvalMaxConcurrency"/> burns the same budget — a saturated gate
|
||||
@@ -85,7 +85,7 @@ public class SiteRuntimeOptions
|
||||
public int TriggerEvalTimeoutSeconds { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1: maximum number of concurrent runs (queued or executing) for any single script
|
||||
/// Maximum number of concurrent runs (queued or executing) for any single script
|
||||
/// or alarm on-trigger script. A trigger that arrives while the cap is reached is SHED —
|
||||
/// the newest run is refused, the four already in flight (which are closest to their own
|
||||
/// deadlines) are kept, and no reordering occurs. A shed increments
|
||||
@@ -125,7 +125,7 @@ public class SiteRuntimeOptions
|
||||
/// <summary>
|
||||
/// Ceiling (ms) of the tag-subscribe retry backoff. Must be >=
|
||||
/// <see cref="TagSubscribeRetryIntervalMs"/>; equal makes the interval fixed
|
||||
/// (pre-Lever-1 behaviour). Default: 60000ms.
|
||||
/// (the original fixed-interval behaviour). Default: 60000ms.
|
||||
/// </summary>
|
||||
public int TagSubscribeRetryMaxIntervalMs { get; set; } = 60000;
|
||||
|
||||
@@ -160,7 +160,7 @@ public class SiteRuntimeOptions
|
||||
public int StuckScriptGraceMs { get; set; } = 30000;
|
||||
|
||||
/// <summary>
|
||||
/// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer):
|
||||
/// Site stream alarm-vs-attribute shared buffer:
|
||||
/// capacity of the bounded hand-off queue feeding <see cref="Streaming.SiteStreamManager"/>'s
|
||||
/// dedicated alarm publish path. Alarm state changes are staged through this
|
||||
/// queue into their OWN broadcast source — separate from the (far higher-volume)
|
||||
@@ -173,7 +173,7 @@ public class SiteRuntimeOptions
|
||||
public int AlarmPublishQueueCapacity { get; set; } = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md):
|
||||
/// Site_events volume policy (docs/plans/2026-08-15-site-events-policy-design.md):
|
||||
/// global switch restoring the legacy per-run Started/Completed Info site events for
|
||||
/// EVERY instance script run. Off by default: those two rows per run were the dominant
|
||||
/// <c>site_events</c> writer (~12 physical row-writes across a replicated pair per run,
|
||||
@@ -188,7 +188,7 @@ public class SiteRuntimeOptions
|
||||
public bool PerRunScriptEvents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// WP3.2: per-script opt-in restoring the legacy per-run Started/Completed Info site
|
||||
/// Per-script opt-in restoring the legacy per-run Started/Completed Info site
|
||||
/// events for NAMED scripts only, for live debugging without flipping the global
|
||||
/// <see cref="PerRunScriptEvents"/> switch (and therefore without paying its volume
|
||||
/// cost for every other script). Effective only while the global switch is off.
|
||||
@@ -201,9 +201,9 @@ public class SiteRuntimeOptions
|
||||
public List<string> PerRunScriptEventScripts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// WP3.2: flush interval (seconds) for <see cref="Scripts.ScriptRunSummaryRecorder"/>'s
|
||||
/// Flush interval (seconds) for <see cref="Scripts.ScriptRunSummaryRecorder"/>'s
|
||||
/// interval aggregate — one "script" Info site event per interval summarizing every
|
||||
/// instance script run since the last flush (headline totals + a top-50-scripts
|
||||
/// instance script run since the last flush (headline totals + a top 50 scripts
|
||||
/// breakdown, capped so a pathological deployment cannot mint an oversized row). An
|
||||
/// interval with zero activity emits no row. <c>0</c> disables the summary flush
|
||||
/// entirely (the recorder still accumulates counters; they are just never emitted or
|
||||
@@ -229,7 +229,7 @@ public class SiteRuntimeOptions
|
||||
/// central believes it replaced).</item>
|
||||
/// </list>
|
||||
/// The sweep is silent when there is nothing to repair. <c>0</c> disables it entirely
|
||||
/// (the row-67 <c>EnsureInstanceStartedCommand</c> path is unaffected). Default: 60.
|
||||
/// (the <c>EnsureInstanceStartedCommand</c> path is unaffected). Default: 60.
|
||||
/// </summary>
|
||||
public int InstanceSweepIntervalSeconds { get; set; } = 60;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer): site-side
|
||||
/// Site-side
|
||||
/// hosted service that periodically reads <see cref="SiteStreamManager.AlarmPublishDroppedCount"/>
|
||||
/// and pushes it into <see cref="ISiteHealthCollector.SetSiteStreamAlarmDropCount"/> so
|
||||
/// the next <see cref="ISiteHealthCollector.CollectReport"/> carries a fresh snapshot on
|
||||
|
||||
@@ -20,8 +20,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming;
|
||||
/// to instance events without referencing SiteRuntime directly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer): attribute
|
||||
/// and alarm events used to share ONE upstream <c>Source.ActorRef</c> + BroadcastHub
|
||||
/// Attribute and alarm events used to share ONE upstream <c>Source.ActorRef</c> + BroadcastHub
|
||||
/// pair, so a burst of attribute changes could fill that shared buffer and evict a
|
||||
/// pending alarm transition before any subscriber ever saw it. Attribute and alarm
|
||||
/// events now travel through entirely SEPARATE publish sources/hubs
|
||||
@@ -48,7 +47,7 @@ public class SiteStreamManager : ISiteStreamSubscriber
|
||||
|
||||
// Attribute-only publish source/hub. By far the higher-volume of the two —
|
||||
// deliberately kept on Source.ActorRef's built-in bounded-mailbox + DropHead
|
||||
// behavior (unchanged from before WP2.6d); losing an occasional attribute update
|
||||
// behavior (unchanged from before the alarm hub split); losing an occasional attribute update
|
||||
// under a storm is the accepted, pre-existing trade-off.
|
||||
private IActorRef? _attributeSourceActor;
|
||||
private Source<ISiteStreamEvent, NotUsed>? _attributeHubSource;
|
||||
@@ -68,13 +67,13 @@ public class SiteStreamManager : ISiteStreamSubscriber
|
||||
|
||||
/// <summary>
|
||||
/// Cumulative count of alarm state changes dropped because
|
||||
/// <see cref="_alarmPublishQueue"/> was at capacity (WP2.6d) — i.e. the site-wide
|
||||
/// <see cref="_alarmPublishQueue"/> was at capacity — i.e. the site-wide
|
||||
/// alarm hand-off queue itself fell behind, not the downstream per-subscriber
|
||||
/// buffers. Surfaced on the site health report.
|
||||
/// </summary>
|
||||
public long AlarmPublishDroppedCount => Interlocked.Read(ref _alarmPublishDroppedCount);
|
||||
|
||||
// Per-source subscriber counts (WP2.6d "skip publish at zero subscribers").
|
||||
// Per-source subscriber counts (skip publish at zero subscribers).
|
||||
// A per-instance Subscribe touches BOTH sources (it needs both attribute and alarm
|
||||
// events for that instance); SubscribeSiteAlarms touches only the alarm source.
|
||||
private int _attributeSubscriberCount;
|
||||
@@ -159,6 +158,9 @@ public class SiteStreamManager : ISiteStreamSubscriber
|
||||
/// wiring can be verified deterministically, without racing the async pump/actor
|
||||
/// system against a burst of publishes.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Maximum number of queued alarm events before the oldest is evicted.</param>
|
||||
/// <param name="onDropped">Callback invoked on every eviction.</param>
|
||||
/// <returns>The bounded alarm hand-off channel.</returns>
|
||||
internal static Channel<AlarmStateChanged> CreateAlarmPublishQueue(int capacity, Action onDropped) =>
|
||||
Channel.CreateBounded<AlarmStateChanged>(
|
||||
new BoundedChannelOptions(Math.Max(1, capacity))
|
||||
@@ -172,7 +174,7 @@ public class SiteStreamManager : ISiteStreamSubscriber
|
||||
/// <summary>
|
||||
/// Publishes an attribute value change to the attribute broadcast hub.
|
||||
/// Fire-and-forget — never blocks the calling actor. Skipped entirely when no
|
||||
/// subscriber is currently interested in attribute events (WP2.6d).
|
||||
/// subscriber is currently interested in attribute events.
|
||||
/// </summary>
|
||||
/// <param name="changed">The attribute value change event to publish.</param>
|
||||
public void PublishAttributeValueChanged(AttributeValueChanged changed)
|
||||
@@ -188,7 +190,7 @@ public class SiteStreamManager : ISiteStreamSubscriber
|
||||
/// from the (far higher-volume) attribute path, so an attribute storm can never
|
||||
/// evict a pending alarm transition. Fire-and-forget — never blocks the calling
|
||||
/// actor. Skipped entirely when no subscriber is currently interested in alarm
|
||||
/// events (WP2.6d).
|
||||
/// events.
|
||||
/// </summary>
|
||||
/// <param name="changed">The alarm state change event to publish.</param>
|
||||
public void PublishAlarmStateChanged(AlarmStateChanged changed)
|
||||
@@ -247,17 +249,7 @@ public class SiteStreamManager : ISiteStreamSubscriber
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to ALARM events for ALL instances on the site (no per-instance
|
||||
/// filter). The dedicated alarm hub carries only <see cref="AlarmStateChanged"/>
|
||||
/// events (the <c>Where</c> below is a defensive no-op, not a load-bearing filter —
|
||||
/// see the type-level remarks); the aggregated Alarm Summary never sees attribute
|
||||
/// events. Same broadcast-hub wiring as <see cref="Subscribe"/>, and the returned
|
||||
/// subscription id is torn down via <see cref="Unsubscribe"/> exactly like the
|
||||
/// per-instance variant.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The actor that receives forwarded <see cref="AlarmStateChanged"/> events.</param>
|
||||
/// <returns>A subscription id to pass to <see cref="Unsubscribe"/>.</returns>
|
||||
/// <inheritdoc />
|
||||
public string SubscribeSiteAlarms(IActorRef subscriber)
|
||||
{
|
||||
if (_alarmHubSource is null || _materializer is null)
|
||||
|
||||
@@ -10,7 +10,7 @@ public class OperationTrackingOptions
|
||||
/// MIGRATION-ONLY as of LocalDb Phase 1. <see cref="OperationTrackingStore"/> no longer
|
||||
/// reads this: the tracking table lives in the consolidated site database at
|
||||
/// <c>LocalDb:Path</c>. The only remaining consumer is
|
||||
/// <c>SiteLocalDbLegacyMigrator</c>, which uses it to locate a pre-Phase-1
|
||||
/// <c>SiteLocalDbLegacyMigrator</c>, which uses it to locate a pre-LocalDb
|
||||
/// <c>site-tracking.db</c> and copy it in once. Safe to delete from a config once that
|
||||
/// node's migration has run.
|
||||
/// </summary>
|
||||
|
||||
@@ -278,6 +278,8 @@ public class OperationTrackingStore : IOperationTrackingStore, IAsyncDisposable,
|
||||
/// take this barrier implicitly; it is public for callers that go around the
|
||||
/// store to the file (tests, tooling) and for anyone who wants "durable now".
|
||||
/// </summary>
|
||||
/// <param name="ct">Token to cancel the wait.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task FlushAsync(CancellationToken ct = default) => WaitForQueuedWritesAsync(ct);
|
||||
|
||||
/// <summary>Queues a lifecycle write and returns once it is queued (not applied).</summary>
|
||||
|
||||
Reference in New Issue
Block a user