Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
272d59c
avoid refcycles in happy eyeballs
graingert Oct 13, 2024
c217d95
update history
graingert Oct 13, 2024
9b46a18
can't clear oserrors
graingert Oct 13, 2024
222c6cd
specify only v4
graingert Oct 13, 2024
a2baf53
fix on trio on py < 3.13
graingert Oct 13, 2024
06c77dc
Merge branch 'master' into happy-eyeballs-cyclic-garbage
graingert Jan 7, 2025
2387f99
Apply suggestions from code review
graingert Jan 7, 2025
0203d03
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 7, 2025
6fba791
fixes after merge
graingert Jan 7, 2025
6dd2010
add ephemeral-port-reserve again
graingert Jan 7, 2025
877855e
Update pyproject.toml
graingert Jan 7, 2025
4bb150d
skip on 3.9 asyncio
graingert Jan 7, 2025
4eea30f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 7, 2025
7ddcd12
Update tests/test_sockets.py
graingert Jan 7, 2025
0542d94
news
graingert Jan 7, 2025
e53e9eb
Update tests/test_sockets.py
graingert Jan 7, 2025
3d626c9
remove ephemeral-port-reserve
graingert Jan 7, 2025
05d9f63
Update tests/test_sockets.py
graingert Jan 7, 2025
72c8b93
Merge branch 'master' into happy-eyeballs-cyclic-garbage
graingert Mar 12, 2025
fa77dd2
Update tests/test_sockets.py
graingert Mar 12, 2025
480b4f8
Merge branch 'master' into happy-eyeballs-cyclic-garbage
graingert Mar 12, 2025
c198531
Merge branch 'master' into happy-eyeballs-cyclic-garbage
graingert Mar 12, 2025
98f0db8
move no_other_refs
graingert Mar 12, 2025
a637d4b
use free_tcp_port
graingert Mar 12, 2025
d1d0e30
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 12, 2025
4fe9de3
fix ip NamedError
graingert Mar 12, 2025
3c99108
Reformatted the changelog entry
agronholm Mar 13, 2025
f167b6f
use free_tcp_port in test_connection_refused
graingert Mar 16, 2025
4f9ff5b
Update tests/test_sockets.py
graingert Mar 16, 2025
fe79118
Update src/anyio/_core/_sockets.py
graingert Mar 16, 2025
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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ test = [
and platform_system != 'Windows' \
and python_version < '3.14'\
"""
"ephemeral-port-reserve >= 1.1.4",
]
doc = [
"packaging",
Expand Down
31 changes: 17 additions & 14 deletions src/anyio/_core/_sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,20 +224,23 @@ async def try_connect(remote_host: str, event: Event) -> None:
target_addrs.append((af, sa[0]))

oserrors: list[OSError] = []
async with create_task_group() as tg:
for i, (af, addr) in enumerate(target_addrs):
event = Event()
tg.start_soon(try_connect, addr, event)
with move_on_after(happy_eyeballs_delay):
await event.wait()

if connected_stream is None:
cause = (
oserrors[0]
if len(oserrors) == 1
else ExceptionGroup("multiple connection attempts failed", oserrors)
)
raise OSError("All connection attempts failed") from cause
try:
async with create_task_group() as tg:
for i, (af, addr) in enumerate(target_addrs):
event = Event()
tg.start_soon(try_connect, addr, event)
with move_on_after(happy_eyeballs_delay):
await event.wait()

if connected_stream is None:
cause = (
oserrors[0]
if len(oserrors) == 1
else ExceptionGroup("multiple connection attempts failed", oserrors)
)
raise OSError("All connection attempts failed") from cause
finally:
oserrors = []

if tls or tls_hostname or ssl_context:
try:
Expand Down
51 changes: 51 additions & 0 deletions tests/test_sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from threading import Thread
from typing import TYPE_CHECKING, Any, Literal, NoReturn, TypeVar, cast

import ephemeral_port_reserve
import psutil
import pytest
from _pytest.fixtures import SubRequest
Expand All @@ -43,6 +44,7 @@
create_unix_datagram_socket,
create_unix_listener,
fail_after,
get_current_task,
getaddrinfo,
getnameinfo,
move_on_after,
Expand All @@ -58,6 +60,7 @@
SocketAttribute,
SocketListener,
SocketStream,
TaskStatus,
)
from anyio.lowlevel import checkpoint
from anyio.streams.stapled import MultiListener
Expand Down Expand Up @@ -133,6 +136,31 @@ def check_asyncio_bug(anyio_backend_name: str, family: AnyIPAddressFamily) -> No
pytest.skip("Does not work due to a known bug (39148)")


if sys.version_info >= (3, 14):

async def no_other_refs() -> list[object]:
frame = sys._getframe(1)
coro = get_current_task().coro

async def get_coro_for_frame(*, task_status: TaskStatus[object]) -> None:
my_coro = coro
while my_coro.cr_frame is not frame:
my_coro = my_coro.cr_await
task_status.started(my_coro)

async with create_task_group() as tg:
return [await tg.start(get_coro_for_frame)]

elif sys.version_info >= (3, 11):

async def no_other_refs() -> list[object]:
return []
else:

async def no_other_refs() -> list[object]:
return [sys._getframe(1)]


_T = TypeVar("_T")


Expand Down Expand Up @@ -315,6 +343,29 @@ def serve() -> None:
server_sock.close()
assert client_addr[0] == expected_client_addr

@pytest.mark.skipif(
sys.implementation.name == "pypy",
reason=(
"gc.get_referrers is broken on PyPy see "
"https://github.com/pypy/pypy/issues/5075"
),
)
async def test_happy_eyeballs_refcycles(self) -> None:
"""
Test derived from https://github.com/python/cpython/pull/124859
"""
ip = "127.0.0.1"
port = ephemeral_port_reserve.reserve(ip=ip)
exc = None
try:
async with await connect_tcp(ip, port):
pass
except OSError as e:
exc = e.__cause__

assert isinstance(exc, OSError)
assert gc.get_referrers(exc) == await no_other_refs()

@pytest.mark.parametrize(
"target, exception_class",
[
Expand Down
Loading