Skip to content

Even more robust graceful close - #620

Open
vdukhovni wants to merge 4 commits into
haskell:masterfrom
vdukhovni:even-more-robust-gracefulClose
Open

Even more robust graceful close#620
vdukhovni wants to merge 4 commits into
haskell:masterfrom
vdukhovni:even-more-robust-gracefulClose

Conversation

@vdukhovni

Copy link
Copy Markdown

This is a proposed fix for the MIO issue in #619
Let's see whether CI agrees.

kazu-yamamoto and others added 4 commits August 17, 2026 07:38
The drain loop makes `gracefulClose` depend on its timeout firing
while `recvBuf` is blocked.  With the MIO manager on Windows,
`recvBuf` blocks in a foreign recv() call, and the asynchronous
exception that `System.Timeout.timeout` throws cannot interrupt a
foreign call.

This is an existing: the timeout in `gracefulClose` hasn't been
able to fire mid-recv under MIO on Windows, since 3.1.1.0.  The
previous single `recvBuf` masked it, because any data from the
peer (not only FIN) completed the call before the timeout was
needed.  The drain loop is the first code path that actually waits
for the FIN.

Enforce the deadline with a watchdog thread instead: `threadDelay`
for the deadline, then shutdown(SHUT_RDWR) on the socket.  Shutdown
aborts a blocked recv while leaving the descriptor valid, so unlike
close it cannot race with a `recvBuf` that has not yet entered the
kernel: whichever side wins, `recvBuf` returns EOF or fails, and
either ends the loop.  The watchdog itself only ever blocks in
`threadDelay`, which is always interruptible, so `killThread`
reliably reaps it once EOF is reached.  No asynchronous exception
ever needs to reach the draining thread, on any platform, so the
same implementation serves everywhere and `timeout` is no longer
used.

Verified on Linux (threaded and non-threaded RTS): the deadline now
bounds a peer that keeps the connection open, and the fast path
(peer's FIN already queued) is unaffected.  Microsoft does not
document the effect of shutdown on an already-blocked recv; this
PR's Windows CI run is the experiment.  Should it not hold,
CancelIoEx on the socket handle is the fallback.

Co-Authored-By: Claude Fable 5
The previous commit's Windows CI run answered the open question in
its message: shutdown() does not wake a recv that is already blocked
on Windows -- the jobs fail exactly as before, with the deadline
never enforced.

Keep the shutdown: it still guarantees that a recv issued after the
deadline fails immediately (WSAESHUTDOWN), closing the race with a
recv that has not yet entered the kernel.  Add CancelIoEx(handle,
NULL) after it to abort a recv that has.  Sockets are created with
WSA_FLAG_OVERLAPPED, so even a blocking recv is an overlapped
operation internally and is cancellable; it fails with
WSA_OPERATION_ABORTED, which ends the drain loop like any other
error.  The descriptor remains valid throughout, so as with
shutdown there is no reuse hazard of the kind close would have.

Under WinIO nothing changes: its overlapped recv already maps
ERROR_OPERATION_ABORTED to EOF, and a cancellation from the
watchdog is handled by the same path.  POSIX platforms are
unchanged: shutdown alone wakes the blocked recv there (verified
on Linux, threaded and non-threaded RTS).

Co-Authored-By: Claude Fable 5
@vdukhovni

Copy link
Copy Markdown
Author

CI now passes! Please feel free to squash the added commits, and apply any other sensible polish.
The watchdog thread approach should be robust going forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants