fix(sessions): refuse late worker attach after close during startup; route open rollback through removal path
A session is registered before its worker is created, so an admin Kill or the host drain could close it during the startup window and the worker client that arrived afterwards was stored on the dead session, MarkReady silently refused, SessionOpened recorded and the Closed session returned to the caller with a live x86 worker nothing would ever stop. AttachWorkerClient now returns false under _syncRoot when the session is Closing/Closed/Faulted or disposed; the manager runs attach + MarkReady + SessionOpened under the per-session close gate, discards (kills and disposes) a refused worker client and fails the open with OpenFailed. OpenRecorded gates the closed-side accounting so a never-opened session is not counted as closed. The open rollback goes through RemoveSessionAsync so per-session event metrics are dropped like every other teardown.
This commit is contained in:
@@ -76,6 +76,49 @@ public sealed class GatewaySessionTests
|
||||
await session.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A worker client that arrives after the session has already been closed (an admin kill
|
||||
/// or the host drain landed while the worker was still starting) must be refused:
|
||||
/// <see cref="GatewaySession.AttachWorkerClient"/> returns <see langword="false"/> and
|
||||
/// leaves <see cref="GatewaySession.WorkerClient"/> unset, so the caller can discard the
|
||||
/// client instead of handing a dead session back as an opened one.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AttachWorkerClient_AfterClose_RefusesLateWorkerClient()
|
||||
{
|
||||
GatewaySession session = CreateSession();
|
||||
session.TransitionTo(SessionState.StartingWorker);
|
||||
|
||||
await session.CloseAsync("admin-kill", CancellationToken.None);
|
||||
bool attached = session.AttachWorkerClient(new BlockingShutdownWorkerClient());
|
||||
|
||||
Assert.False(attached);
|
||||
Assert.Null(session.WorkerClient);
|
||||
Assert.Equal(SessionState.Closed, session.State);
|
||||
|
||||
await session.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposal is the other end of the same window: once the session's distributor has been
|
||||
/// torn down nothing can ever pump a late worker's events, so the attach is refused there
|
||||
/// too rather than storing a client that is never read.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AttachWorkerClient_AfterDispose_RefusesLateWorkerClient()
|
||||
{
|
||||
GatewaySession session = CreateSession();
|
||||
session.TransitionTo(SessionState.StartingWorker);
|
||||
|
||||
await session.DisposeAsync();
|
||||
bool attached = session.AttachWorkerClient(new BlockingShutdownWorkerClient());
|
||||
|
||||
Assert.False(attached);
|
||||
Assert.Null(session.WorkerClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="GatewaySession.MarkFaulted"/> issued
|
||||
/// while <see cref="GatewaySession.CloseAsync"/> is parked between its
|
||||
|
||||
Reference in New Issue
Block a user