fix(siteruntime): injectable ScriptExecutionScheduler + un-leakable expression-fault test
Gitea #18: the process-wide ScriptExecutionScheduler was reached via a static singleton (Shared) directly inside ScriptActor, ScriptExecutionActor, AlarmActor, AlarmExecutionActor, and ScriptSchedulerStatsReporter, so it could not be substituted — a shared mutable global for anything running more than one logical site in a process, most visibly the test assembly. Add an optional scheduler injection seam to all five: the Host injects nothing and gets the shared pool (byte-for-byte unchanged), while a test (or a future multi-site host) hands an actor its own instance. Spawning actors thread their scheduler down to the execution children they create. Shared() now also recreates a disposed cached instance rather than returning it (new IsDisposed guard), so a disposed scheduler can never silently poison every later script/alarm execution. Gitea #16: ScriptActorTests now runs on its OWN scheduler (disposed at class teardown), so the faulted-task test can no longer strand a worker on the process-wide pool and starve unrelated test classes (one prior run failed 22 tests). EvalGate's wait is bounded to 30 s (was unbounded — the actual leak mechanism) and reset per test; the teardown releases one permit per worker instead of Release(Entries), which under-released in exactly the failure case. Full SiteRuntime suite: 6/6 runs green (524/524); was 3/6 failing on clean main. Fixes: #18 Fixes: #16
This commit was merged in pull request #21.
This commit is contained in:
@@ -32,20 +32,30 @@ public sealed class ScriptExecutionScheduler : TaskScheduler, IDisposable
|
||||
private static readonly object SharedLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// The process-wide script-execution scheduler. Lazily created on first use with the
|
||||
/// thread count from <see cref="SiteRuntimeOptions.ScriptExecutionThreadCount"/>; the
|
||||
/// first caller wins, subsequent calls reuse the existing instance.
|
||||
/// The process-wide script-execution scheduler, used as the default when no scheduler
|
||||
/// is injected. Lazily created on first use with the thread count from
|
||||
/// <see cref="SiteRuntimeOptions.ScriptExecutionThreadCount"/>; the first caller wins,
|
||||
/// subsequent calls reuse the existing instance.
|
||||
///
|
||||
/// If the cached instance has been disposed it is recreated rather than handed back:
|
||||
/// a disposed scheduler can execute no work, so returning it would silently poison
|
||||
/// every subsequent script and alarm execution for the rest of the process. Nothing
|
||||
/// disposes the shared instance today, but the guard keeps that latent failure mode
|
||||
/// closed regardless of a future shutdown path or a multi-site host.
|
||||
/// </summary>
|
||||
/// <param name="options">Site runtime options supplying the thread count for the scheduler.</param>
|
||||
/// <returns>The process-wide <see cref="ScriptExecutionScheduler"/> instance, creating it on first call.</returns>
|
||||
/// <returns>A live process-wide <see cref="ScriptExecutionScheduler"/> instance, creating it on first call.</returns>
|
||||
public static ScriptExecutionScheduler Shared(SiteRuntimeOptions options)
|
||||
{
|
||||
if (_shared != null)
|
||||
return _shared;
|
||||
var existing = _shared;
|
||||
if (existing is { IsDisposed: false })
|
||||
return existing;
|
||||
|
||||
lock (SharedLock)
|
||||
{
|
||||
return _shared ??= new ScriptExecutionScheduler(options.ScriptExecutionThreadCount);
|
||||
if (_shared is null || _shared.IsDisposed)
|
||||
_shared = new ScriptExecutionScheduler(options.ScriptExecutionThreadCount);
|
||||
return _shared;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +83,10 @@ public sealed class ScriptExecutionScheduler : TaskScheduler, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary><see langword="true"/> once <see cref="Dispose"/> has run; a disposed scheduler
|
||||
/// can execute no further work and must not be handed out by <see cref="Shared"/>.</summary>
|
||||
public bool IsDisposed => Volatile.Read(ref _disposed) != 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int MaximumConcurrencyLevel => _threads.Count;
|
||||
|
||||
|
||||
@@ -24,21 +24,30 @@ public sealed class ScriptSchedulerStatsReporter : BackgroundService
|
||||
private readonly ILogger<ScriptSchedulerStatsReporter> _logger;
|
||||
private readonly TimeSpan _pollInterval;
|
||||
|
||||
/// <summary>
|
||||
/// Script-execution scheduler seam (#18): 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;
|
||||
|
||||
/// <summary>Initializes a new instance of <see cref="ScriptSchedulerStatsReporter"/>.</summary>
|
||||
/// <param name="collector">The site health collector that receives the scheduler gauges.</param>
|
||||
/// <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>
|
||||
public ScriptSchedulerStatsReporter(
|
||||
ISiteHealthCollector collector,
|
||||
SiteRuntimeOptions options,
|
||||
ILogger<ScriptSchedulerStatsReporter> logger,
|
||||
TimeSpan? pollInterval = null)
|
||||
TimeSpan? pollInterval = null,
|
||||
ScriptExecutionScheduler? scheduler = null)
|
||||
{
|
||||
_collector = collector ?? throw new ArgumentNullException(nameof(collector));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_pollInterval = pollInterval ?? DefaultPollInterval;
|
||||
_scheduler = scheduler;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -67,7 +76,7 @@ public sealed class ScriptSchedulerStatsReporter : BackgroundService
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheduler = ScriptExecutionScheduler.Shared(_options);
|
||||
var scheduler = _scheduler ?? ScriptExecutionScheduler.Shared(_options);
|
||||
_collector.SetScriptSchedulerStats(
|
||||
scheduler.QueueDepth,
|
||||
scheduler.BusyThreadCount,
|
||||
|
||||
Reference in New Issue
Block a user