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:
@@ -40,6 +40,15 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
private readonly ISiteHealthCollector? _healthCollector;
|
||||
private readonly IServiceProvider? _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Script-execution scheduler seam (#18): the process-wide
|
||||
/// <see cref="ScriptExecutionScheduler"/> when null, or an injected instance so
|
||||
/// this actor's trigger-expression evaluation and spawned 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.
|
||||
/// </summary>
|
||||
private readonly ScriptExecutionScheduler? _scheduler;
|
||||
|
||||
private Script<object?>? _compiledScript;
|
||||
private ScriptTriggerConfig? _triggerConfig;
|
||||
private TimeSpan? _minTimeBetweenRuns;
|
||||
@@ -101,6 +110,7 @@ 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>
|
||||
public ScriptActor(
|
||||
string scriptName,
|
||||
string instanceName,
|
||||
@@ -113,7 +123,8 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
Script<object?>? compiledTriggerExpression = null,
|
||||
IReadOnlyDictionary<string, object?>? initialAttributes = null,
|
||||
ISiteHealthCollector? healthCollector = null,
|
||||
IServiceProvider? serviceProvider = null)
|
||||
IServiceProvider? serviceProvider = null,
|
||||
ScriptExecutionScheduler? scheduler = null)
|
||||
{
|
||||
_scriptName = scriptName;
|
||||
_instanceName = instanceName;
|
||||
@@ -124,6 +135,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
_logger = logger;
|
||||
_healthCollector = healthCollector;
|
||||
_serviceProvider = serviceProvider;
|
||||
_scheduler = scheduler;
|
||||
_minTimeBetweenRuns = scriptConfig.MinTimeBetweenRuns;
|
||||
_executionTimeoutSeconds = scriptConfig.ExecutionTimeoutSeconds;
|
||||
_scope = scriptConfig.Scope;
|
||||
@@ -313,7 +325,7 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
return false;
|
||||
}
|
||||
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach,
|
||||
ScriptExecutionScheduler.Shared(_options)).Unwrap().PipeTo(self,
|
||||
_scheduler ?? ScriptExecutionScheduler.Shared(_options)).Unwrap().PipeTo(self,
|
||||
success: r => new ExpressionEvalResult(r),
|
||||
failure: ex => new ExpressionEvalFailed(ex));
|
||||
}
|
||||
@@ -480,7 +492,10 @@ public class ScriptActor : ReceiveActor, IWithTimers
|
||||
// an inbound-API-routed call supplies the inbound request's id.
|
||||
parentExecutionId,
|
||||
// Per-script timeout override (null = use global).
|
||||
_executionTimeoutSeconds));
|
||||
_executionTimeoutSeconds,
|
||||
// Scheduler seam (#18): thread this actor's scheduler override down so
|
||||
// spawned script bodies share the same pool (null = process-wide shared).
|
||||
_scheduler));
|
||||
|
||||
Context.ActorOf(props, executionId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user