Skip to content

Port the give-up API of 3.2.10.0 to 3.2.12.0 to master - #631

Open
kazu-yamamoto wants to merge 15 commits into
haskell:masterfrom
kazu-yamamoto:port-to-master
Open

kazu-yamamoto wants to merge 15 commits into
haskell:masterfrom
kazu-yamamoto:port-to-master

Conversation

@kazu-yamamoto

Copy link
Copy Markdown
Collaborator

This brings the changes released in 3.2.10.0, 3.2.11.0 and 3.2.12.0 from the v3.2 branch to master (3.3.0.0).

  • A blocked socket operation can be given up on through an STM action rather than by closing the socket or interrupting the thread: acceptWith, connectWith, the recv/send families' With versions, withReadable and withWritable.
  • On Windows, a give-up cancels only its own operation (CancelIoEx with its own OVERLAPPED).
  • unsafeClose, unsafeGracefulClose, isClosedSocketError, isTransientAcceptError and isPortUnreachableError.
  • gracefulClose waits for the peer's FIN through recvBufWith on every platform.
  • WinIO: an ICMP unreachable on a datagram socket is raised as WSAECONNRESET instead of being taken as an empty datagram.

The commits are those of v3.2, rebased onto master after #630. CHANGELOG lists them under 3.3.0.0.

🤖 Generated with Claude Code

kazu-yamamoto and others added 15 commits November 4, 2026 13:06
A thread blocked on a socket was stopped either by closing the socket
under it or by an asynchronous exception, and both are going away: GHC
is removing the evtClose delivery closeFdWith relies on, and an
asynchronous exception does not reach a thread blocked in a socket call
on Windows at all.

withReadable takes an STM action beside the operation; whichever settles
first decides, and an operation which has completed wins a tie.
acceptWith, recvBufWith, recvBufFromWith and the ByteString recvWith and
recvFromWith are built from it.  Nothing is closed and no thread is
interrupted to make the caller return.

On Windows there is no readiness to wait for, so the operation runs in a
thread of its own and is ended with CancelIoEx, which leaves the socket
usable; that is Network.Socket.Win32.Cancel.  The four readiness
functions cannot work there and say so rather than blocking for ever.

isClosedSocketError and isTransientAcceptError name what an accept loop
has to tell apart: a socket closed under it, one queued connection which
failed, and the rest.  They read ioe_errno, which on Windows now keeps
the Winsock code rather than dropping it -- Winsock reports every error
as OtherError, so the code was the only thing telling them apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gracefulClose had a code path per platform and a watchdog thread to end
the receive.  recvBufWith replaces all of it: one recvEOF, no GHC.Event,
no System.Timeout, no thread to kill and no CPP.  It keeps the bounded
drain the watchdog scheme grew, because data left unread makes close
send RST rather than FIN, and it therefore keeps to its timeout on
Windows, where System.Timeout cannot cut a receive short at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A datagram sent to a port nobody has bound comes back as an ICMP
unreachable, and Winsock reports it to the next recvfrom even on a
socket which was never connected: one reply bouncing off a client which
had gone ended a server's receive loop.  socket turns that off for a
datagram socket with WSAIoctl(SIO_UDP_CONNRESET, FALSE) and connect
turns it back on, which is where POSIX puts it.

A socket adopted with mkSocket keeps Winsock's default, since nothing
there knows it is a datagram socket.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A bare close(2) where the caller knows no thread is waiting on the socket.
'close' goes through the IO manager to tell a waiter, which locks every
capability and finds nobody.  Doing it outside the library is not possible:
the socket has to be invalidated so that its finalizer does not close the
descriptor a second time, and invalidateSocket is not exported.

(cherry picked from commit 4a2fcae)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
atomically and orElse have been unused since 3.2.10.0; -Wunused-imports
says so on a clean build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit eef109b)
unsafeGracefulClose is gracefulClose ending with unsafeClose, for a
caller which knows no other thread waits on the socket.  recvMsgWith
is recvMsg which the caller can give up on, so that a server reading
control messages can be stopped by being told.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ebffe63)
WSASendMsg returns 0 on success and puts the length in its out
parameter, which was not read, so sendMsg always returned 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
withWritable is withReadable for writing.  On it are built sendBufWith,
sendBufToWith, connectWith, and in Network.Socket.ByteString sendWith,
sendAllWith, sendToWith, sendManyWith and sendMsgWith.  On Windows a
send given up on is cancelled with CancelIoEx, may have sent part of
its data, and leaves the socket unable to send, so a stream given up
on is to be closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
recvWith reads once without waiting, and an error there, such as the
ICMP unreachable of a connected datagram socket, was taken as nothing
to read.  The error is gone once read, so the wait which followed gave
up instead of raising it.  On Windows recvBufNoWait also answered -1
for a recv which failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WINIO receives took WSAECONNRESET for the end of a stream and
returned 0.  On a datagram socket it reports an ICMP unreachable, which
MIO raises and POSIX raises as ECONNREFUSED, so it is raised there too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
recvBufMsgWith and sendBufMsgWith, the give-up functions missing from
Network.Socket.Address (connectWith, sendToWith, sendBufToWith,
recvBufFromWith and the two above), and sendWith, sendAllWith and
recvWith for lazy ByteStrings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sendAllWith took 19.5 us per small send on macOS against sendAll's
2.7 us; with poll(2) first it takes 3.3 us.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A give-up cancelled with CancelIoEx(fd, NULL), which also ended other
threads' operations on the socket.  The give-ups of particular
operations now issue them with an OVERLAPPED of their own (cbits/
overlapped.c) and cancel that one alone.  withReadable and withWritable
cannot, and say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e4a671b)
POSIX raises an ICMP port unreachable as ECONNREFUSED (NoSuchThing) and
Winsock as WSAECONNRESET (OtherError), so the error type cannot tell it
apart portably.  This reads the code.

Co-Authored-By: Claude Opus 5.5 <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