Skip to content

Deliver a receive through STM, and stop pretending readiness works on Windows - #627

Open
kazu-yamamoto wants to merge 1 commit into
haskell:masterfrom
kazu-yamamoto:stm-recv
Open

kazu-yamamoto wants to merge 1 commit into
haskell:masterfrom
kazu-yamamoto:stm-recv

Conversation

@kazu-yamamoto

Copy link
Copy Markdown
Collaborator

The problem

waitReadSocketSTM is threadWaitReadSTM on the raw socket. GHC compiles the
event manager branch out on Windows:

threadWaitRead fd
#if !defined(mingw32_HOST_OS) && !defined(javascript_HOST_ARCH)
  | threaded  = Event.threadWaitRead fd
#endif
  | otherwise = IO $ \s -> ... waitRead# fd# s ...

so it always falls back to the waitRead# primop, which the threaded RTS does
not implement for sockets. The STM action never fires.

Measured on Windows 11 with GHC 9.12.5-rc3, binding a UDP socket, asking for the
STM action and then sending a datagram to it:

--io-manager=posix  (MIO)   : TIMEOUT -- the STM action never fired
--io-manager=native (WinIO) : TIMEOUT -- the STM action never fired
control recvFrom            : Just "hello"   (both)

The control recvFrom picks up the very datagram the STM was waiting for, so the
socket was readable the whole time. This is not a WinIO regression: it is
equally broken under the old I/O manager.
It blocks forever and says nothing.

This is what quic hit. Its server dispatcher waits on waitReadSocketSTM
before every receive, so on Windows it never reaches recvFrom at all and every
connection times out during the handshake.

Why readiness cannot simply be fixed

Completion ports report that an operation finished, not that one could be
started. There is no readiness to wait for.

A zero-byte overlapped WSARecv is the usual way to fake it, and it does detect
arrival — but on a datagram socket it discards the datagram (truncated and
dropped). Verified:

probe waits for data keeps the datagram
0-byte buffer, no flags yes no
0-byte buffer, MSG_PEEK no (completes at once) yes
1-byte buffer, MSG_PEEK yes yes

So it is possible, with a 1-byte MSG_PEEK receive. But it costs an extra
syscall per receive, it has no counterpart for writes, and it is an emulation of
the wrong shape. This PR does not do that.

What this PR does instead

Offer the completion shape, which is what the platform actually provides:

-- Network.Socket.STM
recvBufSTM     :: Socket -> Ptr Word8 -> Int -> IO (STM Int, IO ())
recvBufFromSTM :: SocketAddress sa => Socket -> Ptr Word8 -> Int -> IO (STM (Int, sa), IO ())

-- Network.Socket.ByteString
recvSTM     :: Socket -> Int -> IO (STM ByteString, IO ())
recvFromSTM :: Socket -> Int -> IO (STM (ByteString, SockAddr), IO ())

These start a receive and hand its result over through STM. No platform #if:
on Windows under WinIO the receive goes through withOverlapped, so killThread
triggers CancelIoEx; on POSIX it interrupts threadWaitRead.

On Windows the four readiness functions now fail loudly instead of hanging, and
name what to do instead:

  • waitReadSocketSTM / waitAndCancelReadSocketSTM point at the receives above.
  • waitWriteSocketSTM / waitAndCancelWriteSocketSTM throw as well. Completion
    ports give no way to ask whether a send would block, and none is needed: issue
    the send.

Two caveats are in the Haddock:

  • Cancelling can lose data the kernel has already dequeued, so the STM action
    should not be abandoned casually when composed with orElse.
  • Cancelling needs the receive to be interruptible. It is on POSIX and under
    WinIO, but not under the old Windows I/O manager, where the receive blocks in a
    foreign call that killThread cannot reach. The cancellation test is POSIX
    only for that reason.

Testing

  • macOS: 90 examples, 0 failures (87 on master, plus the three new tests).
  • Windows 11 arm64, GHC 9.12.5-rc3: 84 examples, 0 failures, five runs under
    --io-manager=native and five under --io-manager=posix. (82 on master, plus
    two of the three new tests; the cancellation one is POSIX only.)

Note that CI does not exercise WinIO. configure.ac matches
case "$host_os" in mingw*), but cabal runs configure under an MSYS shell, so
autoconf resolves the host to x86_64-pc-msys and --io-manager=native is never
added. That is a separate problem.

Refs #602.

🤖 Generated with Claude Code

… Windows

waitReadSocketSTM is threadWaitReadSTM on the raw socket.  GHC compiles
the event manager branch of threadWaitRead out on Windows, so it falls
back to the waitRead# primop, which the threaded RTS does not implement
for sockets.  The STM action therefore never fires -- under the old I/O
manager as much as under WinIO, so this is not a WinIO regression.  It
blocks forever and says nothing.

Readiness cannot be recovered on Windows in general: completion ports
report that an operation finished, not that one could be started.  So
offer the completion shape instead.  recvBufSTM and recvBufFromSTM, and
recvSTM and recvFromSTM for ByteStrings, start a receive and hand its
result over through STM.  They work on every platform, and on Windows
they are what callers who used waitReadSocketSTM want.

The four readiness functions are now POSIX only and throw on Windows,
naming what to do instead: use one of the receives above, or, for the
write side, just issue the send -- completion ports give no way to ask
whether one would block and none is needed.

Cancelling a receive can lose data that the kernel has already dequeued,
and needs the receive to be interruptible, which the old Windows I/O
manager does not provide.  Both are documented, and the cancellation
test is POSIX only for the second reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant