feat(site-runtime): stuck-script watchdog names the script holding a scheduler thread past timeout (S2)
This commit is contained in:
@@ -371,4 +371,70 @@ public class ExecutionActorTests : TestKit, IDisposable
|
||||
// Non-positive per-script timeout must be ignored; global (1s) must fire.
|
||||
ExpectTerminated(exec, TimeSpan.FromSeconds(10));
|
||||
}
|
||||
|
||||
// ── S2: stuck-script watchdog names the script holding a scheduler thread ──
|
||||
|
||||
/// <summary>
|
||||
/// Compiles a raw script that can reach the test-assembly <see cref="StuckTestHooks"/>
|
||||
/// (so a script body can block on a gate). Bypasses the trust validator, like
|
||||
/// <see cref="CompileScript"/>.
|
||||
/// </summary>
|
||||
private static Script<object?> CompileRaw(string code)
|
||||
{
|
||||
var scriptOptions = ScriptOptions.Default
|
||||
.WithReferences(typeof(object).Assembly, typeof(Enumerable).Assembly, typeof(StuckTestHooks).Assembly)
|
||||
.WithImports("System", "System.Collections.Generic", "System.Linq", "System.Threading.Tasks");
|
||||
var script = CSharpScript.Create<object?>(code, scriptOptions, typeof(ScriptGlobals));
|
||||
script.Compile();
|
||||
return script;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimedOutScript_WithBlockedThread_EmitsStuckThreadSiteEvent()
|
||||
{
|
||||
// A script that blocks synchronously on a gate NEVER observes cooperative
|
||||
// cancellation, so the CTS firing at the 1s timeout does not free its
|
||||
// scheduler thread. After the 200ms grace the watchdog must name it loudly.
|
||||
StuckTestHooks.Gate = new SemaphoreSlim(0);
|
||||
var compiled = CompileRaw(
|
||||
"ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors.StuckTestHooks.Gate.Wait(); return null;");
|
||||
var replyTo = CreateTestProbe();
|
||||
var instanceActor = CreateTestProbe();
|
||||
var siteLog = new FakeSiteEventLogger();
|
||||
var options = new SiteRuntimeOptions { ScriptExecutionTimeoutSeconds = 1, StuckScriptGraceMs = 200 };
|
||||
|
||||
ActorOf(Props.Create(() => new ScriptExecutionActor(
|
||||
"StuckScript", "Inst1", compiled, null, 0,
|
||||
instanceActor.Ref, _sharedLibrary, options,
|
||||
replyTo.Ref, "corr-stuck", NullLogger.Instance,
|
||||
ScriptScope.Root, null, new SingleServiceProvider(siteLog))));
|
||||
|
||||
try
|
||||
{
|
||||
AwaitAssert(() =>
|
||||
{
|
||||
var rows = siteLog.OfType("script");
|
||||
Assert.Contains(rows, r =>
|
||||
r.Severity == "Error" &&
|
||||
r.Message.Contains("still executing", StringComparison.OrdinalIgnoreCase) &&
|
||||
r.Message.Contains("StuckScript"));
|
||||
}, TimeSpan.FromSeconds(10));
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Free the blocked scheduler thread so the test run stays clean.
|
||||
StuckTestHooks.Gate.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test hook the stuck-script watchdog test uses to block a script-execution
|
||||
/// thread deterministically: the compiled body waits on <see cref="Gate"/>, which
|
||||
/// the test releases once it has observed the stuck-thread site event.
|
||||
/// </summary>
|
||||
public static class StuckTestHooks
|
||||
{
|
||||
/// <summary>Gate a blocking test script waits on; reset per test.</summary>
|
||||
public static SemaphoreSlim Gate = new(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user