Skip to content

Fix UDP socket close hangs on Windows - #1246

Open
Guflly wants to merge 4 commits into
agronholm:masterfrom
Guflly:fix/windows-udp-close
Open

Fix UDP socket close hangs on Windows#1246
Guflly wants to merge 4 commits into
agronholm:masterfrom
Guflly:fix/windows-udp-close

Conversation

@Guflly

@Guflly Guflly commented Jul 27, 2026

Copy link
Copy Markdown

NOTE Erasing or replacing the contents of this template will result in your pull
request being summarily closed without consideration!

Changes

Fixes #1237.

On Windows Proactor event loops, closing a UDP socket while a datagram send is in flight can leave connection_lost() unscheduled and make aclose() wait indefinitely. Follow the transport close with an event-loop turn and abort(), matching the existing stream cleanup path, so the protocol close event is always delivered.

The regression tests cover connected and unconnected UDP sockets on Windows.

Checklist

If this is a user-facing code change, like a bugfix or a new feature, please ensure that
you've fulfilled the following conditions (where applicable):

  • You've added tests (in tests/) which would fail without your patch
  • You've updated the documentation (in docs/), in case of behavior changes or new
    features
  • You've added a new changelog entry (in docs/versionhistory.rst).

If this is a trivial change, like a typo fix or a code reformatting, then you can ignore
these instructions.

Updating the changelog

If there are no entries after the last release, use **UNRELEASED** as the version.
If, say, your patch fixes issue #123, the entry should look like this:

- Fix big bad boo-boo in task groups
  (`#123 <https://github.com/agronholm/anyio/issues/123>`_; PR by @yourgithubaccount)

If there's no issue linked, just link to your pull request instead by updating the
changelog after you've created the PR.

@graingert

graingert commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Can you add a test that:

sock = udp.extra(SocketAttribute.raw_socket)
await anyio.aclose_forcefully(udp)
assert sock.fileno() == -1

@Guflly

Guflly commented Jul 27, 2026

Copy link
Copy Markdown
Author

Added. The test initially left the fd open, so abort now runs in finally and gets a shielded checkpoint. The focused UDP tests pass.

@Guflly
Guflly marked this pull request as ready for review July 27, 2026 21:08
@graingert

Copy link
Copy Markdown
Collaborator

Do SocketStreams have the same problem with aclose_forcefully?

@Guflly

Guflly commented Jul 31, 2026

Copy link
Copy Markdown
Author

Yes, when a send is in flight. I added the same cleanup pattern and a TCP regression test.

@graingert
graingert requested a review from agronholm August 7, 2026 11:12
Comment thread tests/test_sockets.py
with pytest.raises(ClosedResourceError):
await stream.send(b"foo")

@pytest.mark.parametrize("anyio_backend", asyncio_params)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason to limit this to asyncio? Surely we should have the same behaviour on asyncio and trio

Suggested change
@pytest.mark.parametrize("anyio_backend", asyncio_params)

Comment thread tests/test_sockets.py
Comment on lines +1755 to +1756
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
@pytest.mark.parametrize("anyio_backend", asyncio_params)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason to limit this to asyncio on windows? Surely this should pass on non windows and trio

Suggested change
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
@pytest.mark.parametrize("anyio_backend", asyncio_params)

Comment thread tests/test_sockets.py
peer.close()

@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
@pytest.mark.parametrize("anyio_backend", asyncio_params)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@pytest.mark.parametrize("anyio_backend", asyncio_params)

is there a reason to limit this to asyncio on windows? Surely this should pass on non windows and trio

Comment thread docs/versionhistory.rst
module name. (The default name for a task spawned with ``TaskGroup.start_soon`` or
``TaskGroup.start`` typically includes the module name.)
(`#1234 <https://github.com/agronholm/anyio/pull/1234>`_; PR by @gschaffner)
- Fixed UDP socket closing hanging on Windows if a datagram send was still in flight

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth something in the news about fixing TCP's aclose_forcefully

@@ -1687,6 +1690,11 @@
self._closed = True
if not self._transport.is_closing():

@graingert graingert Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is wrong, if aclose_forcefully is called concurrently the second one will return before the socket is closed

@@ -1737,6 +1745,11 @@
self._closed = True
if not self._transport.is_closing():

@graingert graingert Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is wrong, if aclose_forcefully is called concurrently the second one will return before the socket is closed

Comment on lines 1397 to +1410
async def aclose(self) -> None:
self._closed = True
if not self._transport.is_closing():
try:
self._transport.write_eof()
except OSError:
pass

self._transport.close()
await sleep(0)
self._transport.abort()
try:
await sleep(0)
finally:
self._transport.abort()
await AsyncIOBackend.cancel_shielded_checkpoint()

@graingert graingert Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here with concurrent aclose_forcefully I think we want this code:

    async def aclose(self) -> None:
        self._closed = True

        if not self._transport.is_closing():
            try:
                self._transport.write_eof()
            except OSError:
                pass

            self._transport.close()

        try:
            await sleep(0)
        finally:
            self._transport.abort()
            await AsyncIOBackend.cancel_shielded_checkpoint()

graingert-coef added a commit to graingert-coef/anyio that referenced this pull request Aug 13, 2026
The previous versions queued sends and then entered a task group, but
start_soon() only schedules — the children first run when the host yields at
__aexit__, and one event loop iteration is all the proactor needs to reap the
overlapped send. close() then took the no-pending-write path and the tests
passed on Windows for the wrong reason.

anyio puts send()'s checkpoint before transport.sendto(), so the operation is
only still in flight if nothing yields afterwards. Drop the pointless 10x send
loop (each iteration yielded, so the buffer drained every time) and close
directly after a single send, as PR agronholm#1246's own reproducer does.

Adds a concurrent variant gated on an Event so the second closer only runs once
the first has yielded inside aclose(), which is where its unshielded wait on
closed_event is exposed. Neither PR covers that caller.

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.

UDPSocket.aclose() hangs on Windows when a send is in flight (regression from #1147)

2 participants