perf(runtime): split trigger evals from the blocking pool; bounded, deadline-aware execution
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// WP3.1 (finding #4): the process-wide concurrency gate for trigger-expression evaluation.
|
||||
///
|
||||
/// <para>Before WP3.1, <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,
|
||||
/// and the evaluation's own 2 s timeout did not even start until it was dequeued. An alarm
|
||||
/// that should have raised in milliseconds simply never raised.</para>
|
||||
///
|
||||
/// <para>Trigger expressions are non-blocking <em>by construction</em>:
|
||||
/// <see cref="TriggerExpressionGlobals"/> exposes only reads over an in-memory snapshot
|
||||
/// dictionary, and the script trust gate has already denied I/O, network, threading, and
|
||||
/// reflection long before the expression can deploy. They are short CPU-bound work — exactly
|
||||
/// what the shared .NET thread pool is for. So they run there, and this semaphore is the only
|
||||
/// thing bounding their fan-out. A second dedicated pool was considered and rejected: it would
|
||||
/// add threads, gauges, and a second starvation surface for no isolation gain.</para>
|
||||
///
|
||||
/// <para>Per-trigger coalescing in the actors (one evaluation in flight, one pending) already
|
||||
/// caps waiters at one per Expression trigger, so this gate's queue is bounded by trigger
|
||||
/// count.</para>
|
||||
///
|
||||
/// Mirrors <see cref="ScriptExecutionScheduler.Shared"/>'s lazy-singleton plus injectable-seam
|
||||
/// shape so tests can drive a gate of size 1 deterministically.
|
||||
/// </summary>
|
||||
public sealed class TriggerEvalGate : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _gate;
|
||||
|
||||
private static volatile TriggerEvalGate? _shared;
|
||||
private static readonly object SharedLock = new();
|
||||
|
||||
/// <summary>Creates a gate admitting <paramref name="maxConcurrency"/> concurrent evaluations.</summary>
|
||||
/// <param name="maxConcurrency">Maximum concurrent trigger-expression evaluations; values below 1 are clamped to 1.</param>
|
||||
public TriggerEvalGate(int maxConcurrency)
|
||||
{
|
||||
MaxConcurrency = Math.Max(1, maxConcurrency);
|
||||
_gate = new SemaphoreSlim(MaxConcurrency, MaxConcurrency);
|
||||
}
|
||||
|
||||
/// <summary>The configured concurrency limit.</summary>
|
||||
public int MaxConcurrency { get; }
|
||||
|
||||
/// <summary>Free permits right now; 0 means the gate is saturated and new evaluations will queue.</summary>
|
||||
public int AvailablePermits => _gate.CurrentCount;
|
||||
|
||||
/// <summary>
|
||||
/// The process-wide gate, used when no gate is injected. Lazily created from
|
||||
/// <see cref="SiteRuntimeOptions.TriggerEvalMaxConcurrency"/>; the first caller wins.
|
||||
/// </summary>
|
||||
/// <param name="options">Site runtime options supplying the concurrency limit.</param>
|
||||
/// <returns>The shared gate instance.</returns>
|
||||
public static TriggerEvalGate Shared(SiteRuntimeOptions options)
|
||||
{
|
||||
var existing = _shared;
|
||||
if (existing is not null) return existing;
|
||||
|
||||
lock (SharedLock)
|
||||
{
|
||||
return _shared ??= new TriggerEvalGate(options.TriggerEvalMaxConcurrency);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for a permit. The caller passes the evaluation's own deadline token, which was
|
||||
/// armed at ENQUEUE — so time spent queueing here burns the same budget the evaluation
|
||||
/// itself would, and a saturated gate produces a timely cancellation (treated as
|
||||
/// <see langword="false"/> by both actors) instead of an unbounded stall.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The evaluation's deadline token.</param>
|
||||
/// <returns>A task that completes when a permit is acquired.</returns>
|
||||
public Task WaitAsync(CancellationToken cancellationToken) => _gate.WaitAsync(cancellationToken);
|
||||
|
||||
/// <summary>Returns a permit. Must be called exactly once per successful <see cref="WaitAsync"/>.</summary>
|
||||
public void Release() => _gate.Release();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => _gate.Dispose();
|
||||
}
|
||||
Reference in New Issue
Block a user