From df7e20db1dcf5e706bbf25567aed23f3145c4a0b Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 11:20:49 -0400 Subject: [PATCH] test(flaky): deflake SessionManager fail-fast timing assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI (portable, run #38) intermittently failed SessionManagerTests .InvokeAsync_WhenTimeoutZero_FailsFastUnchanged with 'expected immediate fail-fast but took 155ms'. The fail-fast paths throw synchronously under the lock (GatewaySession.GetReadyWorkerClientAsync) and never enter the poll loop, so the absolute <100ms wall-clock bound measured only host load, not behavior, and flaked under CI contention. - WhenWorkerFaulted_FailsFastWithBothStates: anchor the bound to a large (5000ms) ready-wait timeout and assert fail-fast returns in < timeout/3 — a regression that burned the timeout is still caught, but scheduling jitter can't trip it. - WhenTimeoutZero_FailsFastUnchanged: drop the wall-clock assertion entirely (a zero timeout has no wait window to burn); the error code, both-states message, and InvokeCount == 0 already pin the immediate fail-fast. Verified: the 3 SessionManager timing tests pass (net10.0). --- .../Gateway/Sessions/SessionManagerTests.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerTests.cs index 2a0a1af..1ac81c7 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerTests.cs @@ -420,10 +420,16 @@ public sealed class SessionManagerTests [Fact] public async Task InvokeAsync_WhenWorkerFaulted_FailsFastWithBothStates() { + // Use a deliberately large ready-wait timeout so fail-fast is unambiguous: a terminal + // worker must surface immediately instead of burning it. The timing assertion below is + // anchored to a small fraction of this timeout (not an absolute ~100ms wall-clock bound, + // which flaked under CI load) so a regression that waited out the timeout is caught + // while ordinary scheduling jitter never trips it. + const int readyWaitTimeoutMs = 5000; FakeWorkerClient workerClient = new() { State = WorkerClientState.Faulted }; SessionManager manager = CreateManager( new FakeSessionWorkerClientFactory(workerClient), - options: CreateOptions(workerReadyWaitTimeoutMs: 500)); + options: CreateOptions(workerReadyWaitTimeoutMs: readyWaitTimeoutMs)); GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None); Assert.Equal(SessionState.Ready, session.State); @@ -435,7 +441,9 @@ public sealed class SessionManagerTests CancellationToken.None)); stopwatch.Stop(); - Assert.True(stopwatch.ElapsedMilliseconds < 100, $"Expected immediate fail-fast but took {stopwatch.ElapsedMilliseconds}ms."); + Assert.True( + stopwatch.ElapsedMilliseconds < readyWaitTimeoutMs / 3, + $"Expected fail-fast well under the {readyWaitTimeoutMs}ms ready-wait timeout but took {stopwatch.ElapsedMilliseconds}ms."); Assert.Equal(SessionManagerErrorCode.SessionNotReady, exception.ErrorCode); Assert.Contains("Session state is Ready", exception.Message); Assert.Contains("worker state is Faulted", exception.Message); @@ -487,15 +495,17 @@ public sealed class SessionManagerTests GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None); Assert.Equal(SessionState.Ready, session.State); - Stopwatch stopwatch = Stopwatch.StartNew(); + // A zero timeout has no wait window to burn, so there is no wall-clock timing to assert + // (the previous absolute ~100ms bound only measured host load and flaked under CI). The + // error code, the byte-for-byte both-states message, and InvokeCount == 0 fully pin the + // immediate fail-fast — a regression that started polling would still surface the same + // outcome, not a timing difference worth a flaky assertion. SessionManagerException exception = await Assert.ThrowsAsync( async () => await manager.InvokeAsync( session.SessionId, CreateCommand(MxCommandKind.Ping), CancellationToken.None)); - stopwatch.Stop(); - Assert.True(stopwatch.ElapsedMilliseconds < 100, $"Expected immediate fail-fast but took {stopwatch.ElapsedMilliseconds}ms."); Assert.Equal(SessionManagerErrorCode.SessionNotReady, exception.ErrorCode); Assert.Contains("Session state is Ready", exception.Message); Assert.Contains("worker state is Handshaking", exception.Message);