82 lines
4.1 KiB
C#
82 lines
4.1 KiB
C#
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();
|
|
}
|