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..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:
@@ -1687,6 +1690,11 @@ async def aclose(self) -> None:
self._closed = True
if not self._transport.is_closing():
self._transport.close()
+ try:
+ await sleep(0)
+ finally:
+ self._transport.abort()
+ await AsyncIOBackend.cancel_shielded_checkpoint()
await self._protocol.closed_event.wait()
@@ -1737,6 +1745,11 @@ async def aclose(self) -> None:
self._closed = True
if not self._transport.is_closing():
self._transport.close()
+ 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 b456b4079..dbdb7d31f 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,
@@ -518,6 +519,14 @@ 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:
@@ -1743,6 +1752,17 @@ 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")
+ sock = udp.extra(SocketAttribute.raw_socket)
+ await udp.sendto(b"x", "127.0.0.1", 9999)
+ with fail_after(1):
+ await anyio.aclose_forcefully(udp)
+
+ assert sock.fileno() == -1
+
async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None:
async with await create_udp_socket(
family=family, local_host="localhost"
@@ -1914,6 +1934,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