diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs index 1bef756..dca5936 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs @@ -1525,9 +1525,12 @@ public sealed class WorkerPipeSessionTests // activity timestamp stopped advancing would accumulate staleness past the ceiling and // fault — and require the beats to have actually flowed while it ran, so an inspection // that saw a couple of frames and timed out cannot pass for a clean window. None may be a - // WorkerFault while activity is continuously refreshed. The window stays well inside - // FakeRuntimeSession's 5 s dispatch-block ceiling, so the command is still in flight - // throughout. + // WorkerFault while activity is continuously refreshed. Nothing here is racing + // FakeRuntimeSession's blocked-dispatch backstop: that wait is a safety net sized far above + // any window a test opens (and above this test's own cancellation), so the command stays in + // flight for however long a loaded box stretches the loop. Were the two close together, a + // slow run would take the reply mid-window and then fail waiting for a reply already gone + // by — a cancellation at teardown, naming nothing. TimeSpan inspectionWindow = TimeSpan.FromSeconds(2); const int minimumFramesInspected = 30; Stopwatch inspection = Stopwatch.StartNew(); diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/FakeRuntimeSession.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/FakeRuntimeSession.cs index c7b9ebf..0f89658 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/FakeRuntimeSession.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/FakeRuntimeSession.cs @@ -20,6 +20,17 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport; /// internal sealed class FakeRuntimeSession : IWorkerRuntimeSession { + /// + /// Backstop on the wait so a test that never releases leaves no + /// thread parked forever. It is a safety net, never a scenario's timing budget: nothing + /// asserts on it firing, and a test whose blocked window outruns it silently gets its reply + /// mid-window, which then fails as an opaque cancellation somewhere later. Kept far above + /// any test's window — and above the 20 s cancellation those tests arm — so the test's own + /// token always fails first, with its own message. releases the wait + /// regardless, so teardown never depends on this elapsing. + /// + private static readonly TimeSpan BlockedDispatchSafetyNet = TimeSpan.FromSeconds(30); + private readonly ManualResetEventSlim releaseDispatch = new(false); private readonly object gate = new(); private readonly Queue events = new(); @@ -91,7 +102,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession if (BlockDispatch) { - releaseDispatch.Wait(TimeSpan.FromSeconds(5)); + releaseDispatch.Wait(BlockedDispatchSafetyNet); } SetSnapshot(new WorkerRuntimeHeartbeatSnapshot(