Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement happy eyeballs (RFC 8305) #7954

Merged
merged 58 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
8f2b3dc
Implement happy eyeballs
bdraco Dec 9, 2023
fdc3aa0
Implement happy eyeballs
bdraco Dec 9, 2023
2f01917
Implement happy eyeballs
bdraco Dec 9, 2023
be00bc0
cleanups
bdraco Dec 9, 2023
19dfaed
remove from helpers
bdraco Dec 10, 2023
b3b8a09
fix: use real values
bdraco Dec 10, 2023
7ce7a48
fix: mypy
bdraco Dec 10, 2023
3019c25
fix: mypy
bdraco Dec 10, 2023
0e3e296
fix: mypy
bdraco Dec 10, 2023
e66404d
remove unrelated change
bdraco Dec 10, 2023
a5a596a
add interleave support
bdraco Dec 10, 2023
d299e82
add interleave support
bdraco Dec 10, 2023
a1bc0da
mocking fixes
bdraco Dec 10, 2023
b29ae04
fix refactoring error
bdraco Dec 10, 2023
3dc7836
cleanup
bdraco Dec 10, 2023
2b25884
cleanup
bdraco Dec 10, 2023
3e2649f
cleanup
bdraco Dec 10, 2023
31c6fc5
cleanup
bdraco Dec 10, 2023
3dce114
optimize
bdraco Dec 10, 2023
abbfc41
cleanup since other libs need the same code
bdraco Dec 10, 2023
a082a7a
cleanup since other libs need the same code
bdraco Dec 10, 2023
71406c2
Update tests/conftest.py
bdraco Dec 10, 2023
0d2af25
adjust to pass the peer tuple
bdraco Dec 10, 2023
812168b
Merge remote-tracking branch 'upstream/happy_eye_balls' into happy_ey…
bdraco Dec 10, 2023
cf2fd72
empty
bdraco Dec 10, 2023
e7fee0f
empty
bdraco Dec 10, 2023
23c8a60
empty
bdraco Dec 10, 2023
6950c06
Revert "Update tests/conftest.py"
bdraco Dec 11, 2023
e19d3ac
fi xmore mocking
bdraco Dec 11, 2023
d39ed3b
adjust tests
bdraco Dec 11, 2023
7a33cea
revert headers
bdraco Dec 11, 2023
1a72fa8
move non aiohttp specific code to the lib
bdraco Dec 11, 2023
0e8720e
missed one
bdraco Dec 11, 2023
9e36ed1
reduc eoverkill check
bdraco Dec 11, 2023
01f9914
remove unreachable code
bdraco Dec 11, 2023
ec4214d
missed one
bdraco Dec 11, 2023
917500e
timeline
bdraco Dec 11, 2023
f8c3c93
fix race - test left connection open
bdraco Dec 11, 2023
22e0652
fix race - test left connection open
bdraco Dec 11, 2023
3443e16
it should not even be making the connection
bdraco Dec 11, 2023
faad807
maybe leaking from test before
bdraco Dec 11, 2023
bad0f8f
make sure we only try the right family
bdraco Dec 11, 2023
1052d89
fix leak
bdraco Dec 11, 2023
aabc120
family test
bdraco Dec 11, 2023
4d6bb67
fix typing on interleave
bdraco Dec 11, 2023
bdee95e
bump again to make typing agree
bdraco Dec 11, 2023
daa9fa3
empty
bdraco Dec 11, 2023
2d9366f
empty
bdraco Dec 11, 2023
a833f7b
fix typing to turn off happy eye balls
bdraco Dec 11, 2023
676ed0e
parametrize happy_eyeballs_delay
bdraco Dec 11, 2023
b3af81e
Update connector.py
bdraco Dec 11, 2023
3492030
use auto_spec
bdraco Dec 11, 2023
67e0e34
interleave, cleanup tests
bdraco Dec 11, 2023
6288dbf
docs (mostly copied from loop.create_connection)
bdraco Dec 11, 2023
b205d81
tweak
bdraco Dec 11, 2023
655f873
tweak
bdraco Dec 11, 2023
438a047
preen
bdraco Dec 11, 2023
33ca854
bump lib for small performance improvement for 1 addr case
bdraco Dec 12, 2023
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
75 changes: 59 additions & 16 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import functools
import logging
import random
import socket
import sys
import traceback
import warnings
Expand Down Expand Up @@ -31,6 +32,8 @@
cast,
)

import aiohappyeyeballs

from . import hdrs, helpers
from .abc import AbstractResolver
from .client_exceptions import (
Expand Down Expand Up @@ -69,6 +72,39 @@
from .tracing import Trace


def _convert_hosts_to_addr_infos(
hosts: List[Dict[str, Any]]
) -> List[aiohappyeyeballs.AddrInfoType]:
"""Converts the list of hosts to a list of addr_infos.

The list of hosts is the result of a DNS lookup. The list of
addr_infos is the result of a call to `socket.getaddrinfo()`.
"""
addr_infos: List[aiohappyeyeballs.AddrInfoType] = []
for hinfo in hosts:
host = hinfo["host"]
is_ipv6 = helpers.is_ipv6_address(host)
family = socket.AF_INET6 if is_ipv6 else socket.AF_INET
bdraco marked this conversation as resolved.
Show resolved Hide resolved
addr = (host, hinfo["port"], 0, 0) if is_ipv6 else (host, hinfo["port"])
addr_infos.append(
(family, socket.SOCK_STREAM, socket.IPPROTO_TCP, hinfo["hostname"], addr)
)
return addr_infos
bdraco marked this conversation as resolved.
Show resolved Hide resolved


def _convert_local_addr_to_addr_infos(
local_addr: Optional[Tuple[str, int]]
) -> Optional[List[aiohappyeyeballs.AddrInfoType]]:
"""Convert a local_addr tuple to a list of addr_infos."""
if local_addr is None:
return None
host, port = local_addr
is_ipv6 = helpers.is_ipv6_address(host)
family = socket.AF_INET6 if is_ipv6 else socket.AF_INET
addr = (host, port, 0, 0) if is_ipv6 else (host, port)
return [(family, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", addr)]
bdraco marked this conversation as resolved.
Show resolved Hide resolved


class Connection:
_source_traceback = None
_transport = None
Expand Down Expand Up @@ -748,6 +784,8 @@ def __init__(
limit_per_host: int = 0,
enable_cleanup_closed: bool = False,
timeout_ceil_threshold: float = 5,
happy_eyeballs_delay: float = 0.25,
interleave: int = 1,
) -> None:
super().__init__(
keepalive_timeout=keepalive_timeout,
Expand All @@ -772,7 +810,9 @@ def __init__(
self._cached_hosts = _DNSCacheTable(ttl=ttl_dns_cache)
self._throttle_dns_events: Dict[Tuple[str, int], EventResultOrError] = {}
self._family = family
self._local_addr = local_addr
self._local_addr_infos = _convert_local_addr_to_addr_infos(local_addr)
self._happy_eyeballs_delay = happy_eyeballs_delay
self._interleave = interleave

def _close_immediately(self) -> List["asyncio.Future[None]"]:
for ev in self._throttle_dns_events.values():
Expand Down Expand Up @@ -956,6 +996,7 @@ def _get_fingerprint(self, req: ClientRequest) -> Optional["Fingerprint"]:
async def _wrap_create_connection(
self,
*args: Any,
addr_infos: List[aiohappyeyeballs.AddrInfoType],
req: ClientRequest,
timeout: "ClientTimeout",
client_error: Type[Exception] = ClientConnectorError,
Expand All @@ -965,7 +1006,14 @@ async def _wrap_create_connection(
async with ceil_timeout(
timeout.sock_connect, ceil_threshold=timeout.ceil_threshold
):
return await self._loop.create_connection(*args, **kwargs)
sock = await aiohappyeyeballs.start_connection(
addr_infos=addr_infos,
local_addr_infos=self._local_addr_infos,
happy_eyeballs_delay=self._happy_eyeballs_delay,
interleave=self._interleave,
loop=self._loop,
)
return await self._loop.create_connection(*args, **kwargs, sock=sock)
except cert_errors as exc:
raise ClientConnectorCertificateError(req.connection_key, exc) from exc
except ssl_errors as exc:
Expand Down Expand Up @@ -1120,36 +1168,27 @@ def drop_exception(fut: "asyncio.Future[List[Dict[str, Any]]]") -> None:
raise ClientConnectorError(req.connection_key, exc) from exc

last_exc: Optional[Exception] = None

for hinfo in hosts:
host = hinfo["host"]
port = hinfo["port"]

addr_infos = _convert_hosts_to_addr_infos(hosts)
while addr_infos:
# Strip trailing dots, certificates contain FQDN without dots.
# See https://github.com/aio-libs/aiohttp/issues/3636
server_hostname = (
(req.server_hostname or hinfo["hostname"]).rstrip(".")
if sslcontext
else None
(req.server_hostname or host).rstrip(".") if sslcontext else None
)

try:
transp, proto = await self._wrap_create_connection(
self._factory,
host,
port,
timeout=timeout,
ssl=sslcontext,
family=hinfo["family"],
proto=hinfo["proto"],
flags=hinfo["flags"],
addr_infos=addr_infos,
server_hostname=server_hostname,
local_addr=self._local_addr,
req=req,
client_error=client_error,
)
except ClientConnectorError as exc:
last_exc = exc
aiohappyeyeballs.pop_addr_infos_interleave(addr_infos, self._interleave)
continue

if req.is_ssl() and fingerprint:
Expand All @@ -1160,6 +1199,10 @@ def drop_exception(fut: "asyncio.Future[List[Dict[str, Any]]]") -> None:
if not self._cleanup_closed_disabled:
self._cleanup_closed_transports.append(transp)
last_exc = exc
# Remove the bad peer from the list of addr_infos
sock: socket.socket = transp.get_extra_info("socket")
bad_peer = sock.getpeername()
aiohappyeyeballs.remove_addr_infos(addr_infos, bad_peer[0])
bdraco marked this conversation as resolved.
Show resolved Hide resolved
continue

return transp, proto
Expand Down
2 changes: 2 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#
aiodns==3.1.1 ; sys_platform == "linux" or sys_platform == "darwin"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==1.8.1
# via -r requirements/runtime-deps.in
aiosignal==1.3.1
# via -r requirements/runtime-deps.in
async-timeout==4.0.3 ; python_version < "3.11"
Expand Down
8 changes: 5 additions & 3 deletions requirements/constraints.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
# This file is autogenerated by pip-compile with Python 3.8
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/constraints.txt --resolver=backtracking --strip-extras requirements/constraints.in
# pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in
bdraco marked this conversation as resolved.
Show resolved Hide resolved
#
aiodns==3.1.1 ; sys_platform == "linux" or sys_platform == "darwin"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==1.8.1
# via -r requirements/runtime-deps.in
aiohttp-theme==0.1.6
# via -r requirements/doc.in
aioredis==2.0.1
Expand Down
8 changes: 5 additions & 3 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
# This file is autogenerated by pip-compile with Python 3.8
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/dev.txt --resolver=backtracking --strip-extras requirements/dev.in
# pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in
#
bdraco marked this conversation as resolved.
Show resolved Hide resolved
aiodns==3.1.1 ; sys_platform == "linux" or sys_platform == "darwin"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==1.8.1
# via -r requirements/runtime-deps.in
aiohttp-theme==0.1.6
# via -r requirements/doc.in
aioredis==2.0.1
Expand Down
1 change: 1 addition & 0 deletions requirements/runtime-deps.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Extracted from `setup.cfg` via `make sync-direct-runtime-deps`

aiodns >= 1.1; sys_platform=="linux" or sys_platform=="darwin"
aiohappyeyeballs >= 1.8.1
aiosignal >= 1.1.2
async-timeout >= 4.0, < 5.0 ; python_version < "3.11"
Brotli; platform_python_implementation == 'CPython'
Expand Down
2 changes: 2 additions & 0 deletions requirements/runtime-deps.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#
aiodns==3.1.1 ; sys_platform == "linux" or sys_platform == "darwin"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==1.8.1
# via -r requirements/runtime-deps.in
aiosignal==1.3.1
# via -r requirements/runtime-deps.in
async-timeout==4.0.3 ; python_version < "3.11"
Expand Down
6 changes: 4 additions & 2 deletions requirements/test.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#
# This file is autogenerated by pip-compile with python 3.8
# This file is autogenerated by pip-compile with Python 3.8
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/test.txt --resolver=backtracking --strip-extras requirements/test.in
# pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in
#
bdraco marked this conversation as resolved.
Show resolved Hide resolved
aiodns==3.1.1 ; sys_platform == "linux" or sys_platform == "darwin"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==1.8.1
# via -r requirements/runtime-deps.in
aiosignal==1.3.1
# via -r requirements/runtime-deps.in
async-timeout==4.0.3 ; python_version < "3.11"
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ zip_safe = False
include_package_data = True

install_requires =
aiohappyeyeballs >= 1.8.1
aiosignal >= 1.1.2
async-timeout >= 4.0, < 5.0 ; python_version < "3.11"
frozenlist >= 1.1.1
Expand Down
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,11 @@ def netrc_contents(
monkeypatch.setenv("NETRC", str(netrc_file_path))

return netrc_file_path


@pytest.fixture
def start_connection():
with mock.patch(
"aiohttp.connector.aiohappyeyeballs.start_connection"
bdraco marked this conversation as resolved.
Show resolved Hide resolved
) as start_connection_mock:
yield start_connection_mock
bdraco marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading