docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
@@ -46,7 +46,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
// A plain Dictionary read concurrent with a writer's entry reassignment can
|
||||
// throw or return torn state; ConcurrentDictionary makes entry assignment and
|
||||
// snapshot enumeration safe. The only write shapes are indexer-set and Clear,
|
||||
// both of which ConcurrentDictionary supports atomically. (Core.ScriptedAlarms-001)
|
||||
// both of which ConcurrentDictionary supports atomically.
|
||||
private readonly ConcurrentDictionary<string, AlarmState> _alarms = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
@@ -58,7 +58,6 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// runs under <see cref="_evalGate"/>, which serialises every evaluation:
|
||||
/// two threads can never observe the same scratch in a half-refilled state.
|
||||
/// Cleared in <see cref="LoadAsync"/> alongside <see cref="_alarms"/>.
|
||||
/// (Core.ScriptedAlarms-009)
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, AlarmScratch> _scratchByAlarmId =
|
||||
new(StringComparer.Ordinal);
|
||||
@@ -67,11 +66,11 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// Compile cache for every alarm predicate. Routes <see cref="LoadAsync"/>'s
|
||||
/// <see cref="ScriptEvaluator{TContext, TResult}.Compile"/> calls through the
|
||||
/// cache so the collectible <see cref="System.Runtime.Loader.AssemblyLoadContext"/>
|
||||
/// each compile produces is actually disposed on the publish-replace path
|
||||
/// (Core.Scripting-016): the cache's <see cref="CompiledScriptCache{TContext, TResult}.Clear"/>
|
||||
/// each compile produces is actually disposed on the publish-replace path:
|
||||
/// the cache's <see cref="CompiledScriptCache{TContext, TResult}.Clear"/>
|
||||
/// disposes every materialised evaluator before dropping its dictionary entry,
|
||||
/// so a config-publish releases the prior generation's ALCs and the per-publish
|
||||
/// accretion the Core.Scripting-008 fix targeted is actually freed in production.
|
||||
/// accretion the prior fix targeted is actually freed in production.
|
||||
/// Pre-fix the engine called <c>ScriptEvaluator.Compile</c> directly, which left
|
||||
/// the ALCs rooted until the process exited — defeating -008 on the real path.
|
||||
/// </summary>
|
||||
@@ -79,9 +78,9 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
|
||||
/// <summary>
|
||||
/// Test-only diagnostic: returns the per-alarm scratch read-cache dictionary
|
||||
/// if one has been allocated, else null. Used by Core.ScriptedAlarms-009
|
||||
/// regression tests to assert the scratch is reused across evaluations
|
||||
/// (two reads return the same instance).
|
||||
/// if one has been allocated, else null. Used by regression tests to assert
|
||||
/// the scratch is reused across evaluations (two reads return the same
|
||||
/// instance).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Synchronization:</b> the returned <see cref="IReadOnlyDictionary{TKey, TValue}"/>
|
||||
@@ -94,9 +93,10 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// comparisons (e.g. asserting the same instance is reused across calls),
|
||||
/// and single-key reads against an engine that has quiesced after a
|
||||
/// deterministic upstream push. Anything more involved should snapshot a
|
||||
/// copy under the gate. (Core.ScriptedAlarms-013.)
|
||||
/// copy under the gate.
|
||||
/// </remarks>
|
||||
/// <param name="alarmId">The alarm identifier to look up.</param>
|
||||
/// <returns>The alarm's read-cache dictionary if one has been allocated; otherwise null.</returns>
|
||||
internal IReadOnlyDictionary<string, DataValueSnapshot>? TryGetScratchReadCacheForTest(string alarmId)
|
||||
=> _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.ReadCache : null;
|
||||
|
||||
@@ -110,9 +110,9 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// read-cache as <see cref="TryGetScratchReadCacheForTest"/> — the same
|
||||
/// "don't iterate during an in-flight evaluation" caveat applies. Safe
|
||||
/// for reference-identity assertions on a quiesced engine.
|
||||
/// (Core.ScriptedAlarms-013.)
|
||||
/// </remarks>
|
||||
/// <param name="alarmId">The alarm identifier to look up.</param>
|
||||
/// <returns>The alarm's predicate context if one has been allocated; otherwise null.</returns>
|
||||
internal AlarmPredicateContext? TryGetScratchContextForTest(string alarmId)
|
||||
=> _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.Context : null;
|
||||
private readonly ConcurrentDictionary<string, DataValueSnapshot> _valueCache
|
||||
@@ -131,7 +131,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
// these so a re-evaluation in flight when shutdown begins finishes its
|
||||
// SaveAsync before the engine returns control to the caller. The HashSet is
|
||||
// accessed under its own lock — never under _evalGate — so registration /
|
||||
// unregistration cannot deadlock against the gate. (Core.ScriptedAlarms-006)
|
||||
// unregistration cannot deadlock against the gate.
|
||||
private readonly HashSet<Task> _inFlight = [];
|
||||
private readonly object _inFlightLock = new();
|
||||
|
||||
@@ -171,10 +171,11 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// compile failures into one <see cref="InvalidOperationException"/>, subscribes
|
||||
/// to upstream input tags, seeds the value cache, loads persisted state from
|
||||
/// the store (falling back to Fresh for first-load alarms), and recomputes
|
||||
/// ActiveState per Phase 7 plan decision #14 (startup recovery).
|
||||
/// ActiveState from the current predicate (startup recovery).
|
||||
/// </summary>
|
||||
/// <param name="definitions">The alarm definitions to load.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that completes once every definition has been loaded and state restored.</returns>
|
||||
public async Task LoadAsync(IReadOnlyList<ScriptedAlarmDefinition> definitions, CancellationToken ct)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(ScriptedAlarmEngine));
|
||||
@@ -191,7 +192,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
// assignment at the bottom of the try block. Without this, the old timer
|
||||
// keeps firing against the partially-cleared _alarms until Dispose() is
|
||||
// eventually called — not a permanent leak, but an unexpected side effect
|
||||
// during the window. (Core.ScriptedAlarms-015)
|
||||
// during the window.
|
||||
_shelvingTimer?.Dispose();
|
||||
_shelvingTimer = null;
|
||||
|
||||
@@ -200,11 +201,11 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
_alarmsReferencing.Clear();
|
||||
// Drop the prior generation's per-alarm scratch buffers — definitions may
|
||||
// have changed (different Inputs, different Logger), so any reuse would be
|
||||
// unsafe. (Core.ScriptedAlarms-009)
|
||||
// unsafe.
|
||||
_scratchByAlarmId.Clear();
|
||||
// Dispose every compiled-predicate ALC from the prior generation BEFORE we
|
||||
// recompile this one. Skipping this is what made Core.Scripting-008 a
|
||||
// no-op in production. (Core.Scripting-016)
|
||||
// recompile this one. Skipping this is what made the earlier fix a
|
||||
// no-op in production.
|
||||
_compileCache.Clear();
|
||||
|
||||
var compileFailures = new List<string>();
|
||||
@@ -222,7 +223,6 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
|
||||
// Route through CompiledScriptCache so the emitted assembly's
|
||||
// collectible ALC participates in publish-replace cleanup.
|
||||
// (Core.Scripting-016)
|
||||
var evaluator = _compileCache.GetOrCompile(def.PredicateScriptSource);
|
||||
var timed = new TimedScriptEvaluator<AlarmPredicateContext, bool>(evaluator, _scriptTimeout);
|
||||
var logger = _loggerFactory.Create(def.AlarmId);
|
||||
@@ -256,21 +256,20 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
|
||||
// Seed the value cache with current tag values before subscribing. The
|
||||
// ReadTag calls happen first so that the initial predicate evaluation below
|
||||
// (startup recovery, decision #14) uses a consistent snapshot.
|
||||
// (startup recovery) uses a consistent snapshot.
|
||||
// Subscriptions are established AFTER _loaded = true so that any synchronous
|
||||
// initial-push an ITagUpstreamSource delivers from inside SubscribeTag arrives
|
||||
// when _alarms is fully initialised. Before _loaded = true, a synchronous push
|
||||
// would race the in-progress state restore and could overwrite the carefully
|
||||
// seeded cache with a push that has no defined ordering relative to ReadTag.
|
||||
// (Core.ScriptedAlarms-004)
|
||||
foreach (var path in _alarmsReferencing.Keys)
|
||||
_valueCache[path] = _upstream.ReadTag(path);
|
||||
|
||||
// Restore persisted state, falling back to Fresh where nothing was saved,
|
||||
// then re-derive ActiveState from the current predicate per decision #14.
|
||||
// then re-derive ActiveState from the current predicate.
|
||||
// Any predicate emissions queue into `pending` and fire after the gate
|
||||
// is released — so a startup-recovery activation event can call back into
|
||||
// the engine without deadlocking. (Core.ScriptedAlarms-003)
|
||||
// the engine without deadlocking.
|
||||
foreach (var (alarmId, state) in _alarms)
|
||||
{
|
||||
var persisted = await _store.LoadAsync(alarmId, ct).ConfigureAwait(false);
|
||||
@@ -294,8 +293,8 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
|
||||
// Start the shelving-check timer — ticks every 5s, expires any timed shelves
|
||||
// that have passed their UnshelveAtUtc. The prior timer was already disposed
|
||||
// at the START of this try block (Core.ScriptedAlarms-015), so _shelvingTimer
|
||||
// is null here; no double-dispose risk. (Core.ScriptedAlarms-002, -015)
|
||||
// at the START of this try block, so _shelvingTimer
|
||||
// is null here; no double-dispose risk.
|
||||
_shelvingTimer = new Timer(_ => RunShelvingCheck(),
|
||||
null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
|
||||
}
|
||||
@@ -305,7 +304,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
}
|
||||
|
||||
// Fire any emissions collected during startup recovery OUTSIDE the gate so
|
||||
// subscribers can re-enter the engine safely. (Core.ScriptedAlarms-003)
|
||||
// subscribers can re-enter the engine safely.
|
||||
foreach (var evt in pending) FireEvent(evt);
|
||||
}
|
||||
|
||||
@@ -314,10 +313,12 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// unknown alarm. Mainly used for diagnostics + the Admin UI status page.
|
||||
/// </summary>
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <returns>The alarm's current condition state, or null if the alarm id is unknown.</returns>
|
||||
public AlarmConditionState? GetState(string alarmId)
|
||||
=> _alarms.TryGetValue(alarmId, out var s) ? s.Condition : null;
|
||||
|
||||
/// <summary>Gets the current persisted state for all loaded alarms.</summary>
|
||||
/// <returns>The current condition state of every loaded alarm.</returns>
|
||||
public IReadOnlyCollection<AlarmConditionState> GetAllStates()
|
||||
=> _alarms.Values.Select(a => a.Condition).ToArray();
|
||||
|
||||
@@ -326,6 +327,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="user">The user performing the acknowledgment.</param>
|
||||
/// <param name="comment">An optional comment to attach to the acknowledgment.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that completes once the acknowledgment has been applied and persisted.</returns>
|
||||
public Task AcknowledgeAsync(string alarmId, string user, string? comment, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAcknowledge(cur, user, comment, _clock()));
|
||||
|
||||
@@ -334,6 +336,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="user">The user performing the confirmation.</param>
|
||||
/// <param name="comment">An optional comment to attach to the confirmation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that completes once the confirmation has been applied and persisted.</returns>
|
||||
public Task ConfirmAsync(string alarmId, string user, string? comment, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyConfirm(cur, user, comment, _clock()));
|
||||
|
||||
@@ -341,6 +344,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <param name="user">The user performing the shelve operation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that completes once the shelve has been applied and persisted.</returns>
|
||||
public Task OneShotShelveAsync(string alarmId, string user, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyOneShotShelve(cur, user, _clock()));
|
||||
|
||||
@@ -349,6 +353,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="user">The user performing the shelve operation.</param>
|
||||
/// <param name="unshelveAtUtc">The UTC time at which the shelve will automatically expire.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that completes once the timed shelve has been applied and persisted.</returns>
|
||||
public Task TimedShelveAsync(string alarmId, string user, DateTime unshelveAtUtc, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyTimedShelve(cur, user, unshelveAtUtc, _clock()));
|
||||
|
||||
@@ -356,6 +361,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <param name="user">The user performing the unshelve operation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that completes once the unshelve has been applied and persisted.</returns>
|
||||
public Task UnshelveAsync(string alarmId, string user, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyUnshelve(cur, user, _clock()));
|
||||
|
||||
@@ -363,6 +369,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <param name="user">The user performing the enable operation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that completes once the enable has been applied and persisted.</returns>
|
||||
public Task EnableAsync(string alarmId, string user, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyEnable(cur, user, _clock()));
|
||||
|
||||
@@ -370,6 +377,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <param name="user">The user performing the disable operation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that completes once the disable has been applied and persisted.</returns>
|
||||
public Task DisableAsync(string alarmId, string user, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyDisable(cur, user, _clock()));
|
||||
|
||||
@@ -378,6 +386,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="user">The user adding the comment.</param>
|
||||
/// <param name="text">The comment text.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that completes once the comment has been recorded and persisted.</returns>
|
||||
public Task AddCommentAsync(string alarmId, string user, string text, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAddComment(cur, user, text, _clock()));
|
||||
|
||||
@@ -395,14 +404,13 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
// Persist BEFORE updating in-memory so a store failure leaves both
|
||||
// in-memory and persisted at the prior state rather than diverging.
|
||||
// If SaveAsync throws the in-memory _alarms entry stays unchanged and
|
||||
// the exception propagates to the caller. (Core.ScriptedAlarms-007)
|
||||
// the exception propagates to the caller.
|
||||
await _store.SaveAsync(result.State, ct).ConfigureAwait(false);
|
||||
_alarms[alarmId] = state with { Condition = result.State };
|
||||
// Build the emission event under the gate (it captures a coherent
|
||||
// snapshot of state + message-template values) but defer the actual
|
||||
// OnEvent dispatch until after Release() so a slow subscriber or a
|
||||
// subscriber that re-enters the engine doesn't block / deadlock.
|
||||
// (Core.ScriptedAlarms-003)
|
||||
if (result.Emission != EmissionKind.None)
|
||||
pending = BuildEmission(state, result.State, result.Emission);
|
||||
else if (result.NoOpReason is { } reason)
|
||||
@@ -411,7 +419,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
// disabled-alarm no-ops + idempotent ack/confirm/shelve/unshelve
|
||||
// calls. We surface them at debug so they're available when
|
||||
// investigating "why didn't my ack take effect?" without spamming
|
||||
// the main info log. (Core.ScriptedAlarms-011)
|
||||
// the main info log.
|
||||
state.Logger.Debug("Alarm {AlarmId} no-op transition: {Reason}", alarmId, reason);
|
||||
}
|
||||
}
|
||||
@@ -427,7 +435,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// Upstream-change callback. Updates the value cache + enqueues predicate
|
||||
/// re-evaluation for every alarm referencing the changed path. Fire-and-forget
|
||||
/// so driver-side dispatch isn't blocked; the background task is tracked so
|
||||
/// <see cref="Dispose"/> can drain it. (Core.ScriptedAlarms-006)
|
||||
/// <see cref="Dispose"/> can drain it.
|
||||
/// </summary>
|
||||
/// <param name="path">The upstream tag path that changed.</param>
|
||||
/// <param name="value">The new data value snapshot.</param>
|
||||
@@ -452,7 +460,6 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
// Re-check after acquiring the gate: a Dispose() call may have
|
||||
// completed between our _evalGate.WaitAsync and here. Writing to a
|
||||
// disposing store or mutating _alarms after clear is unsafe.
|
||||
// (Core.ScriptedAlarms-005)
|
||||
if (_disposed) return;
|
||||
|
||||
foreach (var id in alarmIds)
|
||||
@@ -463,7 +470,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
if (!ReferenceEquals(newState, state.Condition))
|
||||
{
|
||||
// Persist before updating in-memory so a store failure leaves
|
||||
// both sides at the prior state. (Core.ScriptedAlarms-007)
|
||||
// both sides at the prior state.
|
||||
await _store.SaveAsync(newState, ct).ConfigureAwait(false);
|
||||
_alarms[id] = state with { Condition = newState };
|
||||
}
|
||||
@@ -477,7 +484,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
return;
|
||||
}
|
||||
// Fire emissions OUTSIDE _evalGate so subscriber callbacks can re-enter
|
||||
// the engine without deadlocking. (Core.ScriptedAlarms-003)
|
||||
// the engine without deadlocking.
|
||||
foreach (var evt in pending) FireEvent(evt);
|
||||
}
|
||||
|
||||
@@ -486,14 +493,13 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// Returns the new condition state. If the transition produces an emission,
|
||||
/// appends it to <paramref name="pendingEmissions"/> so the caller can fire
|
||||
/// them after releasing <c>_evalGate</c> — keeping subscriber callbacks
|
||||
/// outside the gate. (Core.ScriptedAlarms-003)
|
||||
/// outside the gate.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every caller (LoadAsync and ReevaluateAsync) owns a <c>pending</c> list and
|
||||
/// passes it here; emissions are always deferred to after the gate is released.
|
||||
/// The parameter is required (non-nullable) to make this contract explicit and
|
||||
/// prevent a future caller from accidentally firing events under the gate.
|
||||
/// (Core.ScriptedAlarms-014)
|
||||
/// </remarks>
|
||||
private async Task<AlarmConditionState> EvaluatePredicateToStateAsync(
|
||||
AlarmState state, AlarmConditionState seed, DateTime nowUtc, CancellationToken ct,
|
||||
@@ -501,7 +507,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
{
|
||||
// Look up (or lazily allocate) the per-alarm scratch and refill its read cache
|
||||
// in place. The dictionary + context survive across evaluations so the hot path
|
||||
// no longer allocates per upstream tag change. (Core.ScriptedAlarms-009)
|
||||
// no longer allocates per upstream tag change.
|
||||
var scratch = _scratchByAlarmId.GetOrAdd(
|
||||
state.Definition.AlarmId,
|
||||
_ => new AlarmScratch(state.Inputs, state.Logger, _clock));
|
||||
@@ -552,7 +558,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// first upstream push hasn't arrived yet. The dictionary is cleared and
|
||||
/// repopulated under <c>_evalGate</c> so no concurrent reader can observe
|
||||
/// a partial state. Replaces the old <c>BuildReadCache</c> which allocated a
|
||||
/// fresh dictionary every call (Core.ScriptedAlarms-009).
|
||||
/// fresh dictionary every call.
|
||||
/// </summary>
|
||||
private void RefillReadCache(
|
||||
Dictionary<string, DataValueSnapshot> cache, IReadOnlySet<string> inputs)
|
||||
@@ -591,7 +597,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <c>_evalGate</c> so the message-template resolution uses a coherent
|
||||
/// value-cache snapshot. The actual <see cref="OnEvent"/> dispatch is
|
||||
/// done by <see cref="FireEvent(ScriptedAlarmEvent)"/> AFTER the gate is
|
||||
/// released. (Core.ScriptedAlarms-003)
|
||||
/// released.
|
||||
/// </summary>
|
||||
private ScriptedAlarmEvent? BuildEmission(AlarmState state, AlarmConditionState condition, EmissionKind kind)
|
||||
{
|
||||
@@ -632,7 +638,6 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// block the gate for every other engine operation, and a subscriber
|
||||
/// that re-enters the engine (e.g. calls AcknowledgeAsync) would
|
||||
/// deadlock against the non-reentrant SemaphoreSlim.
|
||||
/// (Core.ScriptedAlarms-003)
|
||||
/// </summary>
|
||||
private void FireEvent(ScriptedAlarmEvent evt)
|
||||
{
|
||||
@@ -656,7 +661,6 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <summary>
|
||||
/// Register a fire-and-forget task so <see cref="Dispose"/> can await it.
|
||||
/// The task removes itself from the set on completion via a continuation.
|
||||
/// (Core.ScriptedAlarms-006)
|
||||
/// </summary>
|
||||
private void TrackBackgroundTask(Task task)
|
||||
{
|
||||
@@ -673,7 +677,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <summary>
|
||||
/// Test hook — triggers a shelving check synchronously without waiting for
|
||||
/// the 5-second timer. Allows tests that inject a controllable clock to advance
|
||||
/// time and immediately drive timed-shelve expiry. (Core.ScriptedAlarms-012)
|
||||
/// time and immediately drive timed-shelve expiry.
|
||||
/// </summary>
|
||||
internal void RunShelvingCheckForTest() => RunShelvingCheck();
|
||||
|
||||
@@ -689,7 +693,6 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
// running callbacks, so a shelving-check callback that passed the _disposed
|
||||
// check in RunShelvingCheck can arrive here after Dispose() has returned.
|
||||
// Mutating _alarms or saving to a disposed store here is unsafe.
|
||||
// (Core.ScriptedAlarms-005)
|
||||
if (_disposed) return;
|
||||
|
||||
var now = _clock();
|
||||
@@ -700,7 +703,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
if (!ReferenceEquals(result.State, state.Condition))
|
||||
{
|
||||
// Persist before updating in-memory so a store failure leaves
|
||||
// both sides at the prior state. (Core.ScriptedAlarms-007)
|
||||
// both sides at the prior state.
|
||||
await _store.SaveAsync(result.State, ct).ConfigureAwait(false);
|
||||
_alarms[id] = state with { Condition = result.State };
|
||||
if (result.Emission != EmissionKind.None)
|
||||
@@ -718,7 +721,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
_engineLogger.Warning(ex, "ScriptedAlarmEngine shelving-check failed");
|
||||
return;
|
||||
}
|
||||
// Fire emissions OUTSIDE _evalGate. (Core.ScriptedAlarms-003)
|
||||
// Fire emissions OUTSIDE _evalGate.
|
||||
foreach (var evt in pending) FireEvent(evt);
|
||||
}
|
||||
|
||||
@@ -751,7 +754,6 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
// flight can outlive the engine and write to a (possibly disposed) store
|
||||
// after Dispose() has returned. The tasks re-check _disposed after
|
||||
// acquiring the gate and bail out, but the await still has to complete.
|
||||
// (Core.ScriptedAlarms-006)
|
||||
Task[] toAwait;
|
||||
lock (_inFlightLock) { toAwait = [.. _inFlight]; }
|
||||
if (toAwait.Length > 0)
|
||||
@@ -769,20 +771,19 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
// Safe to clear here: the Task.WhenAll drain above guaranteed no
|
||||
// ReevaluateAsync / ShelvingCheckAsync is mid-flight, and _disposed=true
|
||||
// prevents new background work from being queued (OnUpstreamChange bails on
|
||||
// line 334). Pre-Core.Scripting-016 the comment said "Do NOT clear _alarms",
|
||||
// line 334). Previously the comment said "Do NOT clear _alarms",
|
||||
// but that was when the engine called ScriptEvaluator.Compile directly and
|
||||
// held the script ALCs through _alarms→AlarmState→TimedScriptEvaluator
|
||||
// forever — leaving them rooted defeated the -008 collectible-ALC unload.
|
||||
// forever — leaving them rooted defeated the collectible-ALC unload.
|
||||
// Clearing now drops the delegate references so the cache's Dispose call
|
||||
// below can actually unload the emitted assemblies. (Core.ScriptedAlarms-005
|
||||
// re-evaluated under -016.)
|
||||
// below can actually unload the emitted assemblies.
|
||||
_alarms.Clear();
|
||||
_alarmsReferencing.Clear();
|
||||
_scratchByAlarmId.Clear();
|
||||
// Dispose every compiled-predicate ALC so the engine's shutdown actually
|
||||
// releases the emitted assemblies. The drain above ensures no evaluator is
|
||||
// mid-call; CompiledScriptCache.Dispose internally guards against use-after-
|
||||
// dispose. (Core.Scripting-016)
|
||||
// dispose.
|
||||
_compileCache.Dispose();
|
||||
}
|
||||
|
||||
@@ -800,7 +801,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// cleared and refilled in <see cref="ScriptedAlarmEngine.RefillReadCache"/> on
|
||||
/// each call. <see cref="Context"/> wraps that dictionary by reference, so a
|
||||
/// refilled <see cref="ReadCache"/> is what the predicate's
|
||||
/// <c>ctx.GetTag(path)</c> calls observe. (Core.ScriptedAlarms-009)
|
||||
/// <c>ctx.GetTag(path)</c> calls observe.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reuse is safe because <see cref="ScriptedAlarmEngine"/> serialises every
|
||||
|
||||
Reference in New Issue
Block a user