refactor(siteruntime): ScriptExecutionScheduler is an un-substitutable process-wide static with no disposal guard #18
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
ScriptExecutionScheduleris a process-wide static singleton with no reset, no disposal guard, and no injection seam. It works in the Host (one scheduler per process is the intent) but it is un-substitutable and un-resettable, which makes it a shared mutable global for anything that runs more than one logical site in a process — most visibly the test assembly, where it is the mechanism behind #16.Split out of #16, which is about the specific flake. This issue is about the seam itself.
Detail
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptExecutionScheduler.cs:31-53:Consumers call the static directly —
ScriptExecutionActor.cs:115,AlarmExecutionActor.cs:92,AlarmActor.cs:557— so there is no way to hand them a different instance.Three properties follow:
1. First caller wins, so the pool size is set by whoever gets there first.
ScriptExecutionThreadCount(default 8,SiteRuntimeOptions.cs:46) is read only on the first call; every later caller silently gets the first caller's sizing regardless of its own options. In a process hosting one site that is fine and intended. In the test assembly the winner depends on test execution order, so the pool size is not deterministic across runs.2. The class is
IDisposablebut the static is never cleared. Nothing disposes it today (verified). If anything ever did — a test teardown, a future multi-site host, a shutdown path —_sharedwould keep handing out the disposed instance for the remainder of the process, and every subsequent script and alarm execution would fail withObjectDisposedException. There is no guard: no disposed check inShared(), no reset. The failure would be total and confusing, and the codebase already knows this shape (ScriptActor.ExpressionEvalFailedexplicitly cites "ObjectDisposedException from a disposed ScriptExecutionScheduler" — seeAlarmActor.cs:822).3. No seam for tests. A test cannot give an actor its own scheduler, so every script-executing test class in
SiteRuntime.Testsshares one fixed pool of dedicated threads while xUnit runs those classes in parallel. #16 is the consequence: one test parks workers on an unbounded semaphore wait and starves unrelated tests (DeploymentManagerActorTests.RouteInboundApiCall_*time out waiting for a routed script), with one run failing 22 tests.Suggested fix
Give the scheduler an injection seam and let
Shared()stay as the Host's default rather than the only option:Propsinstead of calling the static inside the actors. The Host registers the process-wide instance exactly as today; tests pass their own. This removes the shared-pool coupling for every test, not just #16's.Shared(), so a disposed singleton cannot silently poison the rest of the process.Option 1 also makes the "first caller wins" sizing explicit rather than incidental.
Notes
References
ScriptExecutionScheduler.cs:31-53(the static), consumers atScriptExecutionActor.cs:115,AlarmExecutionActor.cs:92,AlarmActor.cs:557.