From 415551dd6a9c52c285b875fdcca2f77f761316cb Mon Sep 17 00:00:00 2001 From: Guflly <145608489+Guflly@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:06:31 -0700 Subject: [PATCH 1/4] Fix UDP socket close hangs on Windows --- docs/versionhistory.rst | 2 ++ src/anyio/_backends/_asyncio.py | 4 ++++ tests/test_sockets.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index b5b8d01b9..e0205b40c 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -17,6 +17,8 @@ This library adheres to `Semantic Versioning 2.0 `_. module name. (The default name for a task spawned with ``TaskGroup.start_soon`` or ``TaskGroup.start`` typically includes the module name.) (`#1234 `_; PR by @gschaffner) +- Fixed UDP socket closing hanging on Windows if a datagram send was still in flight + (`#1237 `_; PR by @Guflly) - Fixed free-threading compatibility issues arising from the fact that on Python 3.14 free-threading builds, newly created threads inherit the current context by default, causing AnyIO to behave erroneously in relation to ``start_blocking_portal()`` and diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 4fc1f0c64..9b91f5472 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -1687,6 +1687,8 @@ async def aclose(self) -> None: self._closed = True if not self._transport.is_closing(): self._transport.close() + await sleep(0) + self._transport.abort() await self._protocol.closed_event.wait() @@ -1737,6 +1739,8 @@ async def aclose(self) -> None: self._closed = True if not self._transport.is_closing(): self._transport.close() + await sleep(0) + self._transport.abort() await self._protocol.closed_event.wait() diff --git a/tests/test_sockets.py b/tests/test_sockets.py index b456b4079..40c5ede6c 100644 --- a/tests/test_sockets.py +++ b/tests/test_sockets.py @@ -1743,6 +1743,14 @@ async def test_aclose_waits_for_fd_release( udp = await UDPSocket.from_socket(sock) await udp.aclose() + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + @pytest.mark.parametrize("anyio_backend", asyncio_params) + async def test_aclose_during_send(self) -> None: + udp = await create_udp_socket(local_host="127.0.0.1") + await udp.sendto(b"x", "127.0.0.1", 9999) + with fail_after(1): + await udp.aclose() + async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None: async with await create_udp_socket( family=family, local_host="localhost" @@ -1914,6 +1922,14 @@ async def test_aclose_waits_for_fd_release( finally: peer.close() + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + @pytest.mark.parametrize("anyio_backend", asyncio_params) + async def test_aclose_during_send(self) -> None: + udp = await create_connected_udp_socket("127.0.0.1", 9999) + await udp.send(b"x") + with fail_after(1): + await udp.aclose() + async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None: async with await create_connected_udp_socket( "localhost", 5000, family=family From 57cb4a92861992b6ce120979a4acc8b4148f551e Mon Sep 17 00:00:00 2001 From: Guflly <145608489+Guflly@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:07:27 -0700 Subject: [PATCH 2/4] Fix forced UDP socket closure --- src/anyio/_backends/_asyncio.py | 14 ++++++++++---- tests/test_sockets.py | 6 +++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 9b91f5472..8e2bbddd0 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -1687,8 +1687,11 @@ async def aclose(self) -> None: self._closed = True if not self._transport.is_closing(): self._transport.close() - await sleep(0) - self._transport.abort() + try: + await sleep(0) + finally: + self._transport.abort() + await AsyncIOBackend.cancel_shielded_checkpoint() await self._protocol.closed_event.wait() @@ -1739,8 +1742,11 @@ async def aclose(self) -> None: self._closed = True if not self._transport.is_closing(): self._transport.close() - await sleep(0) - self._transport.abort() + try: + await sleep(0) + finally: + self._transport.abort() + await AsyncIOBackend.cancel_shielded_checkpoint() await self._protocol.closed_event.wait() diff --git a/tests/test_sockets.py b/tests/test_sockets.py index 40c5ede6c..64a3fab28 100644 --- a/tests/test_sockets.py +++ b/tests/test_sockets.py @@ -34,6 +34,7 @@ from pytest import FixtureRequest from pytest_mock.plugin import MockerFixture +import anyio from anyio import ( BrokenResourceError, BusyResourceError, @@ -1747,9 +1748,12 @@ async def test_aclose_waits_for_fd_release( @pytest.mark.parametrize("anyio_backend", asyncio_params) async def test_aclose_during_send(self) -> None: udp = await create_udp_socket(local_host="127.0.0.1") + sock = udp.extra(SocketAttribute.raw_socket) await udp.sendto(b"x", "127.0.0.1", 9999) with fail_after(1): - await udp.aclose() + await anyio.aclose_forcefully(udp) + + assert sock.fileno() == -1 async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None: async with await create_udp_socket( From 51e137ebd28ebc09c2ccd1e0d7814df0f68d352a Mon Sep 17 00:00:00 2001 From: maz Date: Fri, 31 Jul 2026 14:20:04 -0700 Subject: [PATCH 3/4] Fix forced TCP socket closure --- src/anyio/_backends/_asyncio.py | 7 +++++-- tests/test_sockets.py | 10 ++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index 8e2bbddd0..3745f99ce 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -1403,8 +1403,11 @@ async def aclose(self) -> None: pass self._transport.close() - await sleep(0) - self._transport.abort() + try: + await sleep(0) + finally: + self._transport.abort() + await AsyncIOBackend.cancel_shielded_checkpoint() class _RawSocketMixin: diff --git a/tests/test_sockets.py b/tests/test_sockets.py index 64a3fab28..5c06602f9 100644 --- a/tests/test_sockets.py +++ b/tests/test_sockets.py @@ -519,6 +519,16 @@ async def test_send_after_close(self, server_addr: tuple[str, int]) -> None: with pytest.raises(ClosedResourceError): await stream.send(b"foo") + @pytest.mark.parametrize("anyio_backend", asyncio_params) + async def test_aclose_forcefully( + self, server_addr: tuple[str, int] + ) -> None: + stream = await connect_tcp(*server_addr) + sock = stream.extra(SocketAttribute.raw_socket) + await stream.send(b"x") + await anyio.aclose_forcefully(stream) + assert sock.fileno() == -1 + async def test_receive_after_peer_closed( self, family: AnyIPAddressFamily, request: FixtureRequest ) -> None: From 7928b000c7b762fd820fb6aeecf58e0fc15a64a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:20:29 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_sockets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_sockets.py b/tests/test_sockets.py index 5c06602f9..dbdb7d31f 100644 --- a/tests/test_sockets.py +++ b/tests/test_sockets.py @@ -520,9 +520,7 @@ async def test_send_after_close(self, server_addr: tuple[str, int]) -> None: await stream.send(b"foo") @pytest.mark.parametrize("anyio_backend", asyncio_params) - async def test_aclose_forcefully( - self, server_addr: tuple[str, int] - ) -> None: + async def test_aclose_forcefully(self, server_addr: tuple[str, int]) -> None: stream = await connect_tcp(*server_addr) sock = stream.extra(SocketAttribute.raw_socket) await stream.send(b"x")