fix(sessions): exception-total shutdown body with non-cancellable kill fallback

This commit is contained in:
Joseph Doherty
2026-08-15 13:19:08 -04:00
parent 0bc13b5292
commit 95f8ba918d
4 changed files with 86 additions and 24 deletions
@@ -1171,6 +1171,33 @@ public sealed class SessionManagerTests
Assert.False(manager.TryGetSession(failingSession.SessionId, out _));
}
/// <summary>
/// A drain whose token is already cancelled (the host stop deadline elapsed) must still
/// kill every worker rather than skip the teardown: an unkilled worker is a leaked x86
/// process, and a restarted gateway terminates orphans instead of reattaching to them. This
/// pins both halves of the fix — the parallel loop is not bound to the caller's token, and
/// the kill fallback does not run on it.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ShutdownAsync_WhenCancelledBeforeDraining_StillKillsEveryWorker()
{
FakeWorkerClient firstClient = new();
FakeWorkerClient secondClient = new();
SessionManager manager = CreateManager(new QueueingSessionWorkerClientFactory(firstClient, secondClient));
GatewaySession firstSession = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None);
GatewaySession secondSession = await manager.OpenSessionAsync(CreateOpenRequest(), "client-2", ownerKeyId: null, CancellationToken.None);
using CancellationTokenSource cancellation = new();
await cancellation.CancelAsync();
await manager.ShutdownAsync(cancellation.Token);
Assert.Equal(1, firstClient.KillCount);
Assert.Equal(1, secondClient.KillCount);
Assert.False(manager.TryGetSession(firstSession.SessionId, out _));
Assert.False(manager.TryGetSession(secondSession.SessionId, out _));
}
/// <summary>Verifies that shutdown closes all registered sessions.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
@@ -1510,10 +1537,12 @@ public sealed class SessionManagerTests
{
await _reached.Task.WaitAsync(RendezvousTimeout, cancellationToken);
}
catch (TimeoutException)
catch (Exception exception) when (exception is TimeoutException or OperationCanceledException)
{
// Sequential teardown: the expected overlap never happens, so let the shutdown
// finish and let MaxObservedConcurrency report the (failing) truth.
// finish and let MaxObservedConcurrency report the (failing) truth. Cancellation is
// swallowed for the same reason — a cancelled rendezvous must not turn into a
// second, misleading failure on top of the concurrency assertion.
}
finally
{