Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
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

(`#1237 <https://github.com/agronholm/anyio/issues/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
Expand Down
17 changes: 15 additions & 2 deletions src/anyio/_backends/_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -1687,6 +1690,11 @@ async def aclose(self) -> None:
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

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

await self._protocol.closed_event.wait()

Expand Down Expand Up @@ -1737,6 +1745,11 @@ async def aclose(self) -> None:
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

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

await self._protocol.closed_event.wait()

Expand Down
28 changes: 28 additions & 0 deletions tests/test_sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from pytest import FixtureRequest
from pytest_mock.plugin import MockerFixture

import anyio
from anyio import (
BrokenResourceError,
BusyResourceError,
Expand Down Expand Up @@ -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)

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)

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:
Expand Down Expand Up @@ -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)
Comment on lines +1755 to +1756

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)

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"
Expand Down Expand Up @@ -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)

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

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
Expand Down
Loading