Skip to content
This repository was archived by the owner on Nov 23, 2017. It is now read-only.

Commit 39c135b

Browse files
committed
Skip getaddrinfo if host is already resolved.
getaddrinfo takes an exclusive lock on some platforms, causing clients to queue up waiting for the lock if many names are being resolved concurrently. Users may want to handle name resolution in their own code, for the sake of caching, using an alternate resolver, or to measure DNS duration separately from connection duration. Skip getaddrinfo if the "host" passed into create_connection is already resolved.
1 parent 74f2d8c commit 39c135b

File tree

7 files changed

+284
-68
lines changed

7 files changed

+284
-68
lines changed

asyncio/base_events.py

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616

1717
import collections
1818
import concurrent.futures
19+
import functools
1920
import heapq
2021
import inspect
22+
import ipaddress
2123
import itertools
2224
import logging
2325
import os
@@ -70,49 +72,83 @@ def _format_pipe(fd):
7072
return repr(fd)
7173

7274

75+
# Linux's sock.type is a bitmask that can include extra info about socket.
76+
_SOCKET_TYPE_MASK = 0
77+
if hasattr(socket, 'SOCK_NONBLOCK'):
78+
_SOCKET_TYPE_MASK |= socket.SOCK_NONBLOCK
79+
if hasattr(socket, 'SOCK_CLOEXEC'):
80+
_SOCKET_TYPE_MASK |= socket.SOCK_CLOEXEC
81+
82+
83+
@functools.lru_cache(maxsize=1024)
84+
def _ipaddr_info(host, port, family, type, proto):
85+
# Try to skip getaddrinfo if "host" is already an IP. Since getaddrinfo
86+
# blocks on an exclusive lock on some platforms, users might handle name
87+
# resolution in their own code and pass in resolved IPs.
88+
if proto not in {0, socket.IPPROTO_TCP, socket.IPPROTO_UDP} or host is None:
89+
return None
90+
91+
type &= ~_SOCKET_TYPE_MASK
92+
if type == socket.SOCK_STREAM:
93+
proto = socket.IPPROTO_TCP
94+
elif type == socket.SOCK_DGRAM:
95+
proto = socket.IPPROTO_UDP
96+
else:
97+
return None
98+
99+
if hasattr(socket, 'inet_pton'):
100+
if family == socket.AF_UNSPEC:
101+
afs = [socket.AF_INET, socket.AF_INET6]
102+
else:
103+
afs = [family]
104+
105+
for af in afs:
106+
# Linux's inet_pton doesn't accept an IPv6 zone index after host,
107+
# like '::1%lo0', so strip it. If we happen to make an invalid
108+
# address look valid, we fail later in sock.connect or sock.bind.
109+
try:
110+
if af == socket.AF_INET6:
111+
socket.inet_pton(af, host.partition('%')[0])
112+
else:
113+
socket.inet_pton(af, host)
114+
return af, type, proto, '', (host, port)
115+
except OSError:
116+
pass
117+
118+
# "host" is not an IP address.
119+
return None
120+
121+
# No inet_pton. (On Windows it's only available since Python 3.4.)
122+
# Even though getaddrinfo with AI_NUMERICHOST would be non-blocking, it
123+
# still requires a lock on some platforms, and waiting for that lock could
124+
# block the event loop. Use ipaddress instead, it's just text parsing.
125+
try:
126+
addr = ipaddress.IPv4Address(host)
127+
except ValueError:
128+
try:
129+
addr = ipaddress.IPv6Address(host.partition('%')[0])
130+
except ValueError:
131+
return None
132+
133+
af = socket.AF_INET if addr.version == 4 else socket.AF_INET6
134+
if family not in (socket.AF_UNSPEC, af):
135+
# "host" is wrong IP version for "family".
136+
return None
137+
138+
return af, type, proto, '', (host, port)
139+
140+
73141
def _check_resolved_address(sock, address):
74142
# Ensure that the address is already resolved to avoid the trap of hanging
75143
# the entire event loop when the address requires doing a DNS lookup.
76-
#
77-
# getaddrinfo() is slow (around 10 us per call): this function should only
78-
# be called in debug mode
79-
family = sock.family
80-
81-
if family == socket.AF_INET:
82-
host, port = address
83-
elif family == socket.AF_INET6:
84-
host, port = address[:2]
85-
else:
144+
145+
if hasattr(socket, 'AF_UNIX') and sock.family == socket.AF_UNIX:
86146
return
87147

88-
# On Windows, socket.inet_pton() is only available since Python 3.4
89-
if hasattr(socket, 'inet_pton'):
90-
# getaddrinfo() is slow and has known issue: prefer inet_pton()
91-
# if available
92-
try:
93-
socket.inet_pton(family, host)
94-
except OSError as exc:
95-
raise ValueError("address must be resolved (IP address), "
96-
"got host %r: %s"
97-
% (host, exc))
98-
else:
99-
# Use getaddrinfo(flags=AI_NUMERICHOST) to ensure that the address is
100-
# already resolved.
101-
type_mask = 0
102-
if hasattr(socket, 'SOCK_NONBLOCK'):
103-
type_mask |= socket.SOCK_NONBLOCK
104-
if hasattr(socket, 'SOCK_CLOEXEC'):
105-
type_mask |= socket.SOCK_CLOEXEC
106-
try:
107-
socket.getaddrinfo(host, port,
108-
family=family,
109-
type=(sock.type & ~type_mask),
110-
proto=sock.proto,
111-
flags=socket.AI_NUMERICHOST)
112-
except socket.gaierror as err:
113-
raise ValueError("address must be resolved (IP address), "
114-
"got host %r: %s"
115-
% (host, err))
148+
host, port = address[:2]
149+
if _ipaddr_info(host, port, sock.family, sock.type, sock.proto) is None:
150+
raise ValueError("address must be resolved (IP address),"
151+
" got host %r" % host)
116152

117153

118154
def _run_until_complete_cb(fut):
@@ -535,7 +571,12 @@ def _getaddrinfo_debug(self, host, port, family, type, proto, flags):
535571

536572
def getaddrinfo(self, host, port, *,
537573
family=0, type=0, proto=0, flags=0):
538-
if self._debug:
574+
info = _ipaddr_info(host, port, family, type, proto)
575+
if info is not None:
576+
fut = futures.Future(loop=self)
577+
fut.set_result([info])
578+
return fut
579+
elif self._debug:
539580
return self.run_in_executor(None, self._getaddrinfo_debug,
540581
host, port, family, type, proto, flags)
541582
else:

asyncio/proactor_events.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,7 @@ def sock_sendall(self, sock, data):
441441

442442
def sock_connect(self, sock, address):
443443
try:
444-
if self._debug:
445-
base_events._check_resolved_address(sock, address)
444+
base_events._check_resolved_address(sock, address)
446445
except ValueError as err:
447446
fut = futures.Future(loop=self)
448447
fut.set_exception(err)

asyncio/selector_events.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,8 +397,7 @@ def sock_connect(self, sock, address):
397397
raise ValueError("the socket must be non-blocking")
398398
fut = futures.Future(loop=self)
399399
try:
400-
if self._debug:
401-
base_events._check_resolved_address(sock, address)
400+
base_events._check_resolved_address(sock, address)
402401
except ValueError as err:
403402
fut.set_exception(err)
404403
else:

asyncio/test_utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,9 +446,14 @@ def disable_logger():
446446
finally:
447447
logger.setLevel(old_level)
448448

449-
def mock_nonblocking_socket():
449+
450+
def mock_nonblocking_socket(proto=socket.IPPROTO_TCP, type=socket.SOCK_STREAM,
451+
family=socket.AF_INET):
450452
"""Create a mock of a non-blocking socket."""
451-
sock = mock.Mock(socket.socket)
453+
sock = mock.MagicMock(socket.socket)
454+
sock.proto = proto
455+
sock.type = type
456+
sock.family = family
452457
sock.gettimeout.return_value = 0.0
453458
return sock
454459

0 commit comments

Comments
 (0)