fix(worker): session-owned stream disposal unblocks and observes the net48 pipe read at teardown
This commit is contained in:
@@ -887,6 +887,36 @@ Graceful shutdown sequence:
|
||||
If shutdown wedges, the gateway kills the process. The worker should be written
|
||||
so process kill does not corrupt other sessions.
|
||||
|
||||
### Ending the pipe read (net48)
|
||||
|
||||
Step 8 above cannot be done by cancellation. On .NET Framework 4.8
|
||||
`NamedPipeClientStream.ReadAsync` accepts a `CancellationToken` and then never
|
||||
wires it to the overlapped I/O, so a read parked waiting for gateway bytes stays
|
||||
parked no matter what the worker cancels. Closing the handle is the only thing
|
||||
that ends it.
|
||||
|
||||
`WorkerPipeSession.RunMessageLoopAsync` races one outstanding read against the
|
||||
heartbeat and event-drain loops, so every fault exit — an event-drain fault, an
|
||||
event too large to frame, a failed heartbeat write — unwinds while that read is
|
||||
still pending. The session therefore owns the transport: `RunAsync`'s outermost
|
||||
`finally` disposes the stream as its last teardown step and then awaits the read
|
||||
that disposal unblocks. Both halves matter. Disposal has to come last because
|
||||
every frame the session will ever write is complete by then (the frame writer
|
||||
signals a write only after it has been written *and* flushed), and the await has
|
||||
to happen while the session still holds the read task, because the worker
|
||||
installs no `TaskScheduler.UnobservedTaskException` handler — otherwise the
|
||||
read's `ObjectDisposedException`/`IOException` lands on a task nobody observes,
|
||||
still holding the reader's reused length-prefix buffer and its pooled payload
|
||||
buffer.
|
||||
|
||||
Two invariants follow. Nothing may call `WorkerFrameReader.ReadAsync` again once
|
||||
a read has been abandoned: a second read would race the first for those buffers
|
||||
and could return a pooled buffer twice. And `WorkerPipeClient`'s `using` on the
|
||||
pipe stays as a backstop for the paths the session never reaches (a session
|
||||
factory that throws), not as the primary owner — disposal is idempotent, so its
|
||||
second `Dispose` is a no-op. Graceful shutdown leaves no pending read at all, so
|
||||
the observation step is a no-op on that path.
|
||||
|
||||
`MxAccessStaSession.ShutdownGracefullyAsync` implements the current cleanup
|
||||
path. It first calls `StaCommandDispatcher.RequestShutdown()` so new commands
|
||||
are rejected and queued commands that have not started receive
|
||||
|
||||
Reference in New Issue
Block a user