test(flaky): deflake SessionManager fail-fast timing assertions
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).
This commit is contained in:
@@ -420,10 +420,16 @@ public sealed class SessionManagerTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeAsync_WhenWorkerFaulted_FailsFastWithBothStates()
|
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 };
|
FakeWorkerClient workerClient = new() { State = WorkerClientState.Faulted };
|
||||||
SessionManager manager = CreateManager(
|
SessionManager manager = CreateManager(
|
||||||
new FakeSessionWorkerClientFactory(workerClient),
|
new FakeSessionWorkerClientFactory(workerClient),
|
||||||
options: CreateOptions(workerReadyWaitTimeoutMs: 500));
|
options: CreateOptions(workerReadyWaitTimeoutMs: readyWaitTimeoutMs));
|
||||||
GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None);
|
GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None);
|
||||||
Assert.Equal(SessionState.Ready, session.State);
|
Assert.Equal(SessionState.Ready, session.State);
|
||||||
|
|
||||||
@@ -435,7 +441,9 @@ public sealed class SessionManagerTests
|
|||||||
CancellationToken.None));
|
CancellationToken.None));
|
||||||
stopwatch.Stop();
|
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.Equal(SessionManagerErrorCode.SessionNotReady, exception.ErrorCode);
|
||||||
Assert.Contains("Session state is Ready", exception.Message);
|
Assert.Contains("Session state is Ready", exception.Message);
|
||||||
Assert.Contains("worker state is Faulted", 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);
|
GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None);
|
||||||
Assert.Equal(SessionState.Ready, session.State);
|
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<SessionManagerException>(
|
SessionManagerException exception = await Assert.ThrowsAsync<SessionManagerException>(
|
||||||
async () => await manager.InvokeAsync(
|
async () => await manager.InvokeAsync(
|
||||||
session.SessionId,
|
session.SessionId,
|
||||||
CreateCommand(MxCommandKind.Ping),
|
CreateCommand(MxCommandKind.Ping),
|
||||||
CancellationToken.None));
|
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.Equal(SessionManagerErrorCode.SessionNotReady, exception.ErrorCode);
|
||||||
Assert.Contains("Session state is Ready", exception.Message);
|
Assert.Contains("Session state is Ready", exception.Message);
|
||||||
Assert.Contains("worker state is Handshaking", exception.Message);
|
Assert.Contains("worker state is Handshaking", exception.Message);
|
||||||
|
|||||||
Reference in New Issue
Block a user