From a755bbbc8e1ecc9e72418fad57764adaa93f48ba Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Thu, 24 May 2018 20:14:30 +0800 Subject: [PATCH 01/22] Implement Happy Eyeballs in asyncio. Added two keyword arguments, `delay` and `interleave`, to `BaseEventLoop.create_connection`. Happy eyeballs is activated if `delay` is specified. --- Lib/asyncio/base_events.py | 138 +++++++++++++++++++++++++---------- Lib/asyncio/events.py | 3 +- Lib/asyncio/helpers.py | 142 +++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 37 deletions(-) create mode 100644 Lib/asyncio/helpers.py diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 09eb440b0ef7af..32c879eb4c67b8 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -16,6 +16,7 @@ import collections import collections.abc import concurrent.futures +import functools import heapq import itertools import logging @@ -38,6 +39,7 @@ from . import coroutines from . import events from . import futures +from . import helpers from . import protocols from . import sslproto from . import tasks @@ -147,6 +149,39 @@ def _ipaddr_info(host, port, family, type, proto): return None +def _roundrobin(*iterables): + """roundrobin('ABC', 'D', 'EF') --> A D E B F C""" + # Copied from Python docs, Recipe credited to George Sakkis + pending = len(iterables) + nexts = itertools.cycle(iter(it).__next__ for it in iterables) + while pending: + try: + for next in nexts: + yield next() + except StopIteration: + pending -= 1 + nexts = itertools.cycle(itertools.islice(nexts, pending)) + + +def _interleave_addrinfos(addrinfos, first_address_family_count=1): + """Interleave list of addrinfo tuples by family.""" + # Group addresses by family + addrinfos_by_family = collections.OrderedDict() + for addr in addrinfos: + family = addr[0] + if family not in addrinfos_by_family: + addrinfos_by_family[family] = [] + addrinfos_by_family[family].append(addr) + addrinfos_lists = list(addrinfos_by_family.values()) + + reordered = [] + if first_address_family_count > 1: + reordered.extend(addrinfos_lists[0][:first_address_family_count - 1]) + del addrinfos_lists[0][:first_address_family_count - 1] + reordered.extend(_roundrobin(*addrinfos_lists)) + return reordered + + def _run_until_complete_cb(fut): if not fut.cancelled(): exc = fut.exception() @@ -839,12 +874,49 @@ def _check_sendfile_params(self, sock, file, offset, count): "offset must be a non-negative integer (got {!r})".format( offset)) + async def _connect_sock(self, exceptions, addr_info, local_addr_infos=None): + """Create, bind and connect one socket.""" + my_exceptions = [] + exceptions.append(my_exceptions) + family, type_, proto, _, address = addr_info + sock = None + try: + sock = socket.socket(family=family, type=type_, proto=proto) + sock.setblocking(False) + if local_addr_infos is not None: + for _, _, _, _, laddr in local_addr_infos: + try: + sock.bind(laddr) + break + except OSError as exc: + msg = ( + f'error while attempting to bind on ' + f'address {laddr!r}: ' + f'{exc.strerror.lower()}' + ) + exc = OSError(exc.errno, msg) + my_exceptions.append(exc) + else: # all bind attempts failed + raise my_exceptions.pop() + await self.sock_connect(sock, address) + return sock + except OSError as exc: + my_exceptions.append(exc) + if sock is not None: + sock.close() + raise + except: + if sock is not None: + sock.close() + raise + async def create_connection( self, protocol_factory, host=None, port=None, *, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, - ssl_handshake_timeout=None): + ssl_handshake_timeout=None, + delay=None, interleave=None): """Connect to a TCP server. Create a streaming transport connection to a given Internet host and @@ -879,6 +951,13 @@ async def create_connection( raise ValueError( 'ssl_handshake_timeout is only meaningful with ssl') + if interleave is not None and delay is None: + raise ValueError('interleave is only meaningful with delay') + + if delay is not None and interleave is None: + # If using happy eyeballs, default to interleave addresses by family + interleave = 1 + if host is not None or port is not None: if sock is not None: raise ValueError( @@ -897,43 +976,30 @@ async def create_connection( flags=flags, loop=self) if not laddr_infos: raise OSError('getaddrinfo() returned empty list') + else: + laddr_infos = None exceptions = [] - for family, type, proto, cname, address in infos: - try: - sock = socket.socket(family=family, type=type, proto=proto) - sock.setblocking(False) - if local_addr is not None: - for _, _, _, _, laddr in laddr_infos: - try: - sock.bind(laddr) - break - except OSError as exc: - msg = ( - f'error while attempting to bind on ' - f'address {laddr!r}: ' - f'{exc.strerror.lower()}' - ) - exc = OSError(exc.errno, msg) - exceptions.append(exc) - else: - sock.close() - sock = None - continue - if self._debug: - logger.debug("connect %r to %r", sock, address) - await self.sock_connect(sock, address) - except OSError as exc: - if sock is not None: - sock.close() - exceptions.append(exc) - except: - if sock is not None: - sock.close() - raise - else: - break - else: + if delay is None: + # not using happy eyeballs + for addrinfo in infos: + try: + sock = await self._connect_sock( + exceptions, addrinfo, laddr_infos) + break + except OSError: + continue + else: # using happy eyeballs + if interleave: + infos = _interleave_addrinfos(infos, interleave) + sock, _, _ = await helpers.staggered_race( + (functools.partial(self._connect_sock, + exceptions, addrinfo, laddr_infos) + for addrinfo in infos), + delay, loop=self) + + if sock is None: + exceptions = [exc for sub in exceptions for exc in sub] if len(exceptions) == 1: raise exceptions[0] else: diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index 40946bbf65299d..33f461114b7669 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -305,7 +305,8 @@ async def create_connection( *, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, - ssl_handshake_timeout=None): + ssl_handshake_timeout=None, + delay=None, interleave=None): raise NotImplementedError async def create_server( diff --git a/Lib/asyncio/helpers.py b/Lib/asyncio/helpers.py new file mode 100644 index 00000000000000..b0a7e431ac48fc --- /dev/null +++ b/Lib/asyncio/helpers.py @@ -0,0 +1,142 @@ +from contextlib import suppress +from typing import Iterable, Callable, Any, Tuple, List, Optional, Awaitable + +from . import events +from . import futures +from . import locks +from . import tasks + + +async def staggered_race( + coro_fns: Iterable[Callable[[], Awaitable]], + delay: Optional[float], + *, + loop: events.AbstractEventLoop = None, +) -> Tuple[ + Any, + Optional[int], + List[Optional[Exception]] +]: + """Run coroutines with staggered start times and take the first to finish. + + This method takes an iterable of coroutine functions. The first one is + started immediately. From then on, whenever the immediately preceding one + fails (raises an exception), or when *delay* seconds has passed, the next + coroutine is started. This continues until one of the coroutines complete + successfully, in which case all others are cancelled, or until all + coroutines fail. + + The coroutines provided should be well-behaved in the following way: + + * They should only ``return`` if completed successfully. + + * They should always raise an exception if they did not complete + successfully. In particular, if they handle cancellation, they should + probably reraise, like this:: + + try: + # do work + except asyncio.CancelledError: + # undo partially completed work + raise + + Args: + coro_fns: an iterable of coroutine functions, i.e. callables that + return a coroutine object when called. Use ``functools.partial`` or + lambdas to pass arguments. + + delay: amount of time, in seconds, between starting coroutines. If + ``None``, the coroutines will run sequentially. + + loop: the event loop to use. + + Returns: + tuple *(winner_result, winner_index, exceptions)* where + + - *winner_result*: the result of the winning coroutine, or ``None`` + if no coroutines won. + + - *winner_index*: the index of the winning coroutine in + ``coro_fns``, or ``None`` if no coroutines won. If the winning + coroutine may return None on success, *winner_index* can be used + to definitively determine whether any coroutine won. + + - *exceptions*: list of exceptions returned by the coroutines. + ``len(exceptions)`` is equal to the number of coroutines actually + started, and the order is the same as in ``coro_fns``. The winning + coroutine's entry is ``None``. + + """ + # TODO: allow async iterables in coro_fns. + loop = loop or events.get_running_loop() + enum_coro_fns = enumerate(coro_fns) + winner_result = None + winner_index = None + exceptions = [] + running_tasks = [] + + async def run_one_coro(previous_failed: Optional[locks.Event]) -> None: + # Wait for the previous task to finish, or for delay seconds + if previous_failed is not None: + with suppress(futures.TimeoutError): + # Use asyncio.wait_for() instead of asyncio.wait() here, so + # that if we get cancelled at this point, Event.wait() is also + # cancelled, otherwise there will be a "Task destroyed but it is + # pending" later. + await tasks.wait_for(previous_failed.wait(), delay) + # Get the next coroutine to run + try: + this_index, coro_fn = next(enum_coro_fns) + except StopIteration: + return + # Start task that will run the next coroutine + this_failed = locks.Event() + next_task = loop.create_task(run_one_coro(this_failed)) + running_tasks.append(next_task) + assert len(running_tasks) == this_index + 2 + # Prepare place to put this coroutine's exceptions if not won + exceptions.append(None) + assert len(exceptions) == this_index + 1 + + try: + result = await coro_fn() + except Exception as e: + exceptions[this_index] = e + this_failed.set() # Kickstart the next coroutine + else: + # Store winner's results + nonlocal winner_index, winner_result + assert winner_index is None + winner_index = this_index + winner_result = result + # Cancel all other tasks. We take care to not cancel the current + # task as well. If we do so, then since there is no `await` after + # here and CancelledError are usually thrown at one, we will + # encounter a curious corner case where the current task will end + # up as done() == True, cancelled() == False, exception() == + # asyncio.CancelledError, which is normally not possible. + # https://bugs.python.org/issue33413 + for i, t in enumerate(running_tasks): + if i != this_index: + t.cancel() + + first_task = loop.create_task(run_one_coro(None)) + running_tasks.append(first_task) + try: + # Wait for a growing list of tasks to all finish: poor man's version of + # curio's TaskGroup or trio's nursery + done_count = 0 + while done_count != len(running_tasks): + done, _ = await tasks.wait(running_tasks) + done_count = len(done) + # If run_one_coro raises an unhandled exception, it's probably a + # programming error, and I want to see it. + if __debug__: + for d in done: + if d.done() and not d.cancelled() and d.exception(): + raise d.exception() + return winner_result, winner_index, exceptions + finally: + # Make sure no tasks are left running if we leave this function + for t in running_tasks: + t.cancel() From fc2945064f0da51d67f5bf574e4d3867943678af Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 17:55:16 +0800 Subject: [PATCH 02/22] Use module import instead of individual imports. --- Lib/asyncio/helpers.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Lib/asyncio/helpers.py b/Lib/asyncio/helpers.py index b0a7e431ac48fc..5819d399e3fdce 100644 --- a/Lib/asyncio/helpers.py +++ b/Lib/asyncio/helpers.py @@ -1,5 +1,5 @@ -from contextlib import suppress -from typing import Iterable, Callable, Any, Tuple, List, Optional, Awaitable +import contextlib +import typing from . import events from . import futures @@ -8,14 +8,14 @@ async def staggered_race( - coro_fns: Iterable[Callable[[], Awaitable]], - delay: Optional[float], + coro_fns: typing.Iterable[typing.Callable[[], typing.Awaitable]], + delay: typing.Optional[float], *, loop: events.AbstractEventLoop = None, -) -> Tuple[ - Any, - Optional[int], - List[Optional[Exception]] +) -> typing.Tuple[ + typing.Any, + typing.Optional[int], + typing.List[typing.Optional[Exception]] ]: """Run coroutines with staggered start times and take the first to finish. @@ -75,10 +75,11 @@ async def staggered_race( exceptions = [] running_tasks = [] - async def run_one_coro(previous_failed: Optional[locks.Event]) -> None: + async def run_one_coro( + previous_failed: typing.Optional[locks.Event]) -> None: # Wait for the previous task to finish, or for delay seconds if previous_failed is not None: - with suppress(futures.TimeoutError): + with contextlib.suppress(futures.TimeoutError): # Use asyncio.wait_for() instead of asyncio.wait() here, so # that if we get cancelled at this point, Event.wait() is also # cancelled, otherwise there will be a "Task destroyed but it is From d792c43b3f1e3cd8e42703cab3de2cd5f5099617 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 17:56:10 +0800 Subject: [PATCH 03/22] Change TODO. --- Lib/asyncio/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/helpers.py b/Lib/asyncio/helpers.py index 5819d399e3fdce..5a3c223b7ac657 100644 --- a/Lib/asyncio/helpers.py +++ b/Lib/asyncio/helpers.py @@ -67,7 +67,7 @@ async def staggered_race( coroutine's entry is ``None``. """ - # TODO: allow async iterables in coro_fns. + # TODO: when we have aiter() and anext(), allow async iterables in coro_fns. loop = loop or events.get_running_loop() enum_coro_fns = enumerate(coro_fns) winner_result = None From f9111d0250c183f7cd5348f2c109bc699868fa98 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 17:57:59 +0800 Subject: [PATCH 04/22] Rename helpers.py to staggered.py. --- Lib/asyncio/base_events.py | 4 ++-- Lib/asyncio/{helpers.py => staggered.py} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename Lib/asyncio/{helpers.py => staggered.py} (100%) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 32c879eb4c67b8..2a406d7c757df0 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -39,9 +39,9 @@ from . import coroutines from . import events from . import futures -from . import helpers from . import protocols from . import sslproto +from . import staggered from . import tasks from . import transports from .log import logger @@ -992,7 +992,7 @@ async def create_connection( else: # using happy eyeballs if interleave: infos = _interleave_addrinfos(infos, interleave) - sock, _, _ = await helpers.staggered_race( + sock, _, _ = await staggered.staggered_race( (functools.partial(self._connect_sock, exceptions, addrinfo, laddr_infos) for addrinfo in infos), diff --git a/Lib/asyncio/helpers.py b/Lib/asyncio/staggered.py similarity index 100% rename from Lib/asyncio/helpers.py rename to Lib/asyncio/staggered.py From ded34e00b0ba18e18b465f0e1449c85bf45659f3 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 18:28:31 +0800 Subject: [PATCH 05/22] Add create_connection()'s new arguments in documentation. --- Doc/library/asyncio-eventloop.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 9d7f2362b3d19b..a418674b9f1daf 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -340,9 +340,23 @@ Creating connections If given, these should all be integers from the corresponding :mod:`socket` module constants. + * *delay*, if given, enables Happy Eyeballs for this connection. It should + be a floating-point number representing the amount of time in seconds + to wait for a connection attempt to complete, before starting the next + attempt in parallel. This is the "Connection Attempt Delay" as defined + in RFC 8305. + + * *interleave*, only for use together with *delay*, controls address + reordering. If ``0`` is specified, no reordering is done, and addresses are + tried in the order returned by :meth:`getaddrinfo`. If a positive integer + is specified, the addresses are interleaved by address family, and the + given integer is interpreted as "First Address Family Count" as defined + in RFC 8305. The default is ``1``. + * *sock*, if given, should be an existing, already connected :class:`socket.socket` object to be used by the transport. - If *sock* is given, none of *host*, *port*, *family*, *proto*, *flags* + If *sock* is given, none of *host*, *port*, *family*, *proto*, *flags*, + *delay*, *interleave* and *local_addr* should be specified. * *local_addr*, if given, is a ``(local_host, local_port)`` tuple used @@ -353,6 +367,10 @@ Creating connections to wait for the SSL handshake to complete before aborting the connection. ``10.0`` seconds if ``None`` (default). + .. versionadded:: 3.8 + + The *delay* and *interleave* parameters. + .. versionadded:: 3.7 The *ssl_handshake_timeout* parameter. From b069c95a43b733110e8ba5c58ec36a1bcfe728ab Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 18:35:28 +0800 Subject: [PATCH 06/22] Add blurb. --- .../next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst diff --git a/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst b/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst new file mode 100644 index 00000000000000..086d28fa770eaf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst @@ -0,0 +1,2 @@ +Implemented Happy Eyeballs in `asyncio.create_connection()`. Added two new +arguments, *delay* and *interleave*, to specify Happy Eyeballs behavior. From c5d3a92cf7f5dba8b6c3813b8befba7cdef0ab89 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Thu, 24 May 2018 20:14:30 +0800 Subject: [PATCH 07/22] Implement Happy Eyeballs in asyncio. Added two keyword arguments, `delay` and `interleave`, to `BaseEventLoop.create_connection`. Happy eyeballs is activated if `delay` is specified. --- Lib/asyncio/base_events.py | 138 +++++++++++++++++++++++++---------- Lib/asyncio/events.py | 3 +- Lib/asyncio/helpers.py | 142 +++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 37 deletions(-) create mode 100644 Lib/asyncio/helpers.py diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 61938e90c375df..6da7c8bce512a0 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -16,6 +16,7 @@ import collections import collections.abc import concurrent.futures +import functools import heapq import itertools import logging @@ -38,6 +39,7 @@ from . import coroutines from . import events from . import futures +from . import helpers from . import protocols from . import sslproto from . import tasks @@ -147,6 +149,39 @@ def _ipaddr_info(host, port, family, type, proto): return None +def _roundrobin(*iterables): + """roundrobin('ABC', 'D', 'EF') --> A D E B F C""" + # Copied from Python docs, Recipe credited to George Sakkis + pending = len(iterables) + nexts = itertools.cycle(iter(it).__next__ for it in iterables) + while pending: + try: + for next in nexts: + yield next() + except StopIteration: + pending -= 1 + nexts = itertools.cycle(itertools.islice(nexts, pending)) + + +def _interleave_addrinfos(addrinfos, first_address_family_count=1): + """Interleave list of addrinfo tuples by family.""" + # Group addresses by family + addrinfos_by_family = collections.OrderedDict() + for addr in addrinfos: + family = addr[0] + if family not in addrinfos_by_family: + addrinfos_by_family[family] = [] + addrinfos_by_family[family].append(addr) + addrinfos_lists = list(addrinfos_by_family.values()) + + reordered = [] + if first_address_family_count > 1: + reordered.extend(addrinfos_lists[0][:first_address_family_count - 1]) + del addrinfos_lists[0][:first_address_family_count - 1] + reordered.extend(_roundrobin(*addrinfos_lists)) + return reordered + + def _run_until_complete_cb(fut): if not fut.cancelled(): exc = fut.exception() @@ -844,12 +879,49 @@ def _check_sendfile_params(self, sock, file, offset, count): "offset must be a non-negative integer (got {!r})".format( offset)) + async def _connect_sock(self, exceptions, addr_info, local_addr_infos=None): + """Create, bind and connect one socket.""" + my_exceptions = [] + exceptions.append(my_exceptions) + family, type_, proto, _, address = addr_info + sock = None + try: + sock = socket.socket(family=family, type=type_, proto=proto) + sock.setblocking(False) + if local_addr_infos is not None: + for _, _, _, _, laddr in local_addr_infos: + try: + sock.bind(laddr) + break + except OSError as exc: + msg = ( + f'error while attempting to bind on ' + f'address {laddr!r}: ' + f'{exc.strerror.lower()}' + ) + exc = OSError(exc.errno, msg) + my_exceptions.append(exc) + else: # all bind attempts failed + raise my_exceptions.pop() + await self.sock_connect(sock, address) + return sock + except OSError as exc: + my_exceptions.append(exc) + if sock is not None: + sock.close() + raise + except: + if sock is not None: + sock.close() + raise + async def create_connection( self, protocol_factory, host=None, port=None, *, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, - ssl_handshake_timeout=None): + ssl_handshake_timeout=None, + delay=None, interleave=None): """Connect to a TCP server. Create a streaming transport connection to a given Internet host and @@ -884,6 +956,13 @@ async def create_connection( raise ValueError( 'ssl_handshake_timeout is only meaningful with ssl') + if interleave is not None and delay is None: + raise ValueError('interleave is only meaningful with delay') + + if delay is not None and interleave is None: + # If using happy eyeballs, default to interleave addresses by family + interleave = 1 + if host is not None or port is not None: if sock is not None: raise ValueError( @@ -902,43 +981,30 @@ async def create_connection( flags=flags, loop=self) if not laddr_infos: raise OSError('getaddrinfo() returned empty list') + else: + laddr_infos = None exceptions = [] - for family, type, proto, cname, address in infos: - try: - sock = socket.socket(family=family, type=type, proto=proto) - sock.setblocking(False) - if local_addr is not None: - for _, _, _, _, laddr in laddr_infos: - try: - sock.bind(laddr) - break - except OSError as exc: - msg = ( - f'error while attempting to bind on ' - f'address {laddr!r}: ' - f'{exc.strerror.lower()}' - ) - exc = OSError(exc.errno, msg) - exceptions.append(exc) - else: - sock.close() - sock = None - continue - if self._debug: - logger.debug("connect %r to %r", sock, address) - await self.sock_connect(sock, address) - except OSError as exc: - if sock is not None: - sock.close() - exceptions.append(exc) - except: - if sock is not None: - sock.close() - raise - else: - break - else: + if delay is None: + # not using happy eyeballs + for addrinfo in infos: + try: + sock = await self._connect_sock( + exceptions, addrinfo, laddr_infos) + break + except OSError: + continue + else: # using happy eyeballs + if interleave: + infos = _interleave_addrinfos(infos, interleave) + sock, _, _ = await helpers.staggered_race( + (functools.partial(self._connect_sock, + exceptions, addrinfo, laddr_infos) + for addrinfo in infos), + delay, loop=self) + + if sock is None: + exceptions = [exc for sub in exceptions for exc in sub] if len(exceptions) == 1: raise exceptions[0] else: diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index 40946bbf65299d..33f461114b7669 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -305,7 +305,8 @@ async def create_connection( *, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, - ssl_handshake_timeout=None): + ssl_handshake_timeout=None, + delay=None, interleave=None): raise NotImplementedError async def create_server( diff --git a/Lib/asyncio/helpers.py b/Lib/asyncio/helpers.py new file mode 100644 index 00000000000000..b0a7e431ac48fc --- /dev/null +++ b/Lib/asyncio/helpers.py @@ -0,0 +1,142 @@ +from contextlib import suppress +from typing import Iterable, Callable, Any, Tuple, List, Optional, Awaitable + +from . import events +from . import futures +from . import locks +from . import tasks + + +async def staggered_race( + coro_fns: Iterable[Callable[[], Awaitable]], + delay: Optional[float], + *, + loop: events.AbstractEventLoop = None, +) -> Tuple[ + Any, + Optional[int], + List[Optional[Exception]] +]: + """Run coroutines with staggered start times and take the first to finish. + + This method takes an iterable of coroutine functions. The first one is + started immediately. From then on, whenever the immediately preceding one + fails (raises an exception), or when *delay* seconds has passed, the next + coroutine is started. This continues until one of the coroutines complete + successfully, in which case all others are cancelled, or until all + coroutines fail. + + The coroutines provided should be well-behaved in the following way: + + * They should only ``return`` if completed successfully. + + * They should always raise an exception if they did not complete + successfully. In particular, if they handle cancellation, they should + probably reraise, like this:: + + try: + # do work + except asyncio.CancelledError: + # undo partially completed work + raise + + Args: + coro_fns: an iterable of coroutine functions, i.e. callables that + return a coroutine object when called. Use ``functools.partial`` or + lambdas to pass arguments. + + delay: amount of time, in seconds, between starting coroutines. If + ``None``, the coroutines will run sequentially. + + loop: the event loop to use. + + Returns: + tuple *(winner_result, winner_index, exceptions)* where + + - *winner_result*: the result of the winning coroutine, or ``None`` + if no coroutines won. + + - *winner_index*: the index of the winning coroutine in + ``coro_fns``, or ``None`` if no coroutines won. If the winning + coroutine may return None on success, *winner_index* can be used + to definitively determine whether any coroutine won. + + - *exceptions*: list of exceptions returned by the coroutines. + ``len(exceptions)`` is equal to the number of coroutines actually + started, and the order is the same as in ``coro_fns``. The winning + coroutine's entry is ``None``. + + """ + # TODO: allow async iterables in coro_fns. + loop = loop or events.get_running_loop() + enum_coro_fns = enumerate(coro_fns) + winner_result = None + winner_index = None + exceptions = [] + running_tasks = [] + + async def run_one_coro(previous_failed: Optional[locks.Event]) -> None: + # Wait for the previous task to finish, or for delay seconds + if previous_failed is not None: + with suppress(futures.TimeoutError): + # Use asyncio.wait_for() instead of asyncio.wait() here, so + # that if we get cancelled at this point, Event.wait() is also + # cancelled, otherwise there will be a "Task destroyed but it is + # pending" later. + await tasks.wait_for(previous_failed.wait(), delay) + # Get the next coroutine to run + try: + this_index, coro_fn = next(enum_coro_fns) + except StopIteration: + return + # Start task that will run the next coroutine + this_failed = locks.Event() + next_task = loop.create_task(run_one_coro(this_failed)) + running_tasks.append(next_task) + assert len(running_tasks) == this_index + 2 + # Prepare place to put this coroutine's exceptions if not won + exceptions.append(None) + assert len(exceptions) == this_index + 1 + + try: + result = await coro_fn() + except Exception as e: + exceptions[this_index] = e + this_failed.set() # Kickstart the next coroutine + else: + # Store winner's results + nonlocal winner_index, winner_result + assert winner_index is None + winner_index = this_index + winner_result = result + # Cancel all other tasks. We take care to not cancel the current + # task as well. If we do so, then since there is no `await` after + # here and CancelledError are usually thrown at one, we will + # encounter a curious corner case where the current task will end + # up as done() == True, cancelled() == False, exception() == + # asyncio.CancelledError, which is normally not possible. + # https://bugs.python.org/issue33413 + for i, t in enumerate(running_tasks): + if i != this_index: + t.cancel() + + first_task = loop.create_task(run_one_coro(None)) + running_tasks.append(first_task) + try: + # Wait for a growing list of tasks to all finish: poor man's version of + # curio's TaskGroup or trio's nursery + done_count = 0 + while done_count != len(running_tasks): + done, _ = await tasks.wait(running_tasks) + done_count = len(done) + # If run_one_coro raises an unhandled exception, it's probably a + # programming error, and I want to see it. + if __debug__: + for d in done: + if d.done() and not d.cancelled() and d.exception(): + raise d.exception() + return winner_result, winner_index, exceptions + finally: + # Make sure no tasks are left running if we leave this function + for t in running_tasks: + t.cancel() From 38c7caa05cf65c59b1d4a23df4b18cda7686ca6c Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 17:55:16 +0800 Subject: [PATCH 08/22] Use module import instead of individual imports. --- Lib/asyncio/helpers.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Lib/asyncio/helpers.py b/Lib/asyncio/helpers.py index b0a7e431ac48fc..5819d399e3fdce 100644 --- a/Lib/asyncio/helpers.py +++ b/Lib/asyncio/helpers.py @@ -1,5 +1,5 @@ -from contextlib import suppress -from typing import Iterable, Callable, Any, Tuple, List, Optional, Awaitable +import contextlib +import typing from . import events from . import futures @@ -8,14 +8,14 @@ async def staggered_race( - coro_fns: Iterable[Callable[[], Awaitable]], - delay: Optional[float], + coro_fns: typing.Iterable[typing.Callable[[], typing.Awaitable]], + delay: typing.Optional[float], *, loop: events.AbstractEventLoop = None, -) -> Tuple[ - Any, - Optional[int], - List[Optional[Exception]] +) -> typing.Tuple[ + typing.Any, + typing.Optional[int], + typing.List[typing.Optional[Exception]] ]: """Run coroutines with staggered start times and take the first to finish. @@ -75,10 +75,11 @@ async def staggered_race( exceptions = [] running_tasks = [] - async def run_one_coro(previous_failed: Optional[locks.Event]) -> None: + async def run_one_coro( + previous_failed: typing.Optional[locks.Event]) -> None: # Wait for the previous task to finish, or for delay seconds if previous_failed is not None: - with suppress(futures.TimeoutError): + with contextlib.suppress(futures.TimeoutError): # Use asyncio.wait_for() instead of asyncio.wait() here, so # that if we get cancelled at this point, Event.wait() is also # cancelled, otherwise there will be a "Task destroyed but it is From 70ec96d2086074098056eb9f64b2ecef754ce3bd Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 17:56:10 +0800 Subject: [PATCH 09/22] Change TODO. --- Lib/asyncio/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/helpers.py b/Lib/asyncio/helpers.py index 5819d399e3fdce..5a3c223b7ac657 100644 --- a/Lib/asyncio/helpers.py +++ b/Lib/asyncio/helpers.py @@ -67,7 +67,7 @@ async def staggered_race( coroutine's entry is ``None``. """ - # TODO: allow async iterables in coro_fns. + # TODO: when we have aiter() and anext(), allow async iterables in coro_fns. loop = loop or events.get_running_loop() enum_coro_fns = enumerate(coro_fns) winner_result = None From cef0a766676f47c75251e5549e38a1d316880924 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 17:57:59 +0800 Subject: [PATCH 10/22] Rename helpers.py to staggered.py. --- Lib/asyncio/base_events.py | 4 ++-- Lib/asyncio/{helpers.py => staggered.py} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename Lib/asyncio/{helpers.py => staggered.py} (100%) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 6da7c8bce512a0..8e30da089d8e94 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -39,9 +39,9 @@ from . import coroutines from . import events from . import futures -from . import helpers from . import protocols from . import sslproto +from . import staggered from . import tasks from . import transports from .log import logger @@ -997,7 +997,7 @@ async def create_connection( else: # using happy eyeballs if interleave: infos = _interleave_addrinfos(infos, interleave) - sock, _, _ = await helpers.staggered_race( + sock, _, _ = await staggered.staggered_race( (functools.partial(self._connect_sock, exceptions, addrinfo, laddr_infos) for addrinfo in infos), diff --git a/Lib/asyncio/helpers.py b/Lib/asyncio/staggered.py similarity index 100% rename from Lib/asyncio/helpers.py rename to Lib/asyncio/staggered.py From 73a4a5a296427fcf1fa212e45ce3985a7b6d5940 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 18:28:31 +0800 Subject: [PATCH 11/22] Add create_connection()'s new arguments in documentation. --- Doc/library/asyncio-eventloop.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 9d7f2362b3d19b..a418674b9f1daf 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -340,9 +340,23 @@ Creating connections If given, these should all be integers from the corresponding :mod:`socket` module constants. + * *delay*, if given, enables Happy Eyeballs for this connection. It should + be a floating-point number representing the amount of time in seconds + to wait for a connection attempt to complete, before starting the next + attempt in parallel. This is the "Connection Attempt Delay" as defined + in RFC 8305. + + * *interleave*, only for use together with *delay*, controls address + reordering. If ``0`` is specified, no reordering is done, and addresses are + tried in the order returned by :meth:`getaddrinfo`. If a positive integer + is specified, the addresses are interleaved by address family, and the + given integer is interpreted as "First Address Family Count" as defined + in RFC 8305. The default is ``1``. + * *sock*, if given, should be an existing, already connected :class:`socket.socket` object to be used by the transport. - If *sock* is given, none of *host*, *port*, *family*, *proto*, *flags* + If *sock* is given, none of *host*, *port*, *family*, *proto*, *flags*, + *delay*, *interleave* and *local_addr* should be specified. * *local_addr*, if given, is a ``(local_host, local_port)`` tuple used @@ -353,6 +367,10 @@ Creating connections to wait for the SSL handshake to complete before aborting the connection. ``10.0`` seconds if ``None`` (default). + .. versionadded:: 3.8 + + The *delay* and *interleave* parameters. + .. versionadded:: 3.7 The *ssl_handshake_timeout* parameter. From b3a6e1cf7c341b8146ae9ef39a0b9e173df1a918 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 29 May 2018 18:35:28 +0800 Subject: [PATCH 12/22] Add blurb. --- .../next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst diff --git a/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst b/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst new file mode 100644 index 00000000000000..086d28fa770eaf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst @@ -0,0 +1,2 @@ +Implemented Happy Eyeballs in `asyncio.create_connection()`. Added two new +arguments, *delay* and *interleave*, to specify Happy Eyeballs behavior. From b8d7e416df9dbdaf1e6775af29966054786dadf7 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Sun, 3 Jun 2018 15:23:34 +0800 Subject: [PATCH 13/22] Remove _roundrobin as a standalone function. A simpler version is now folded into _interleave_addrinfos. --- Lib/asyncio/base_events.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 8e30da089d8e94..7587dd7a6391e5 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -149,20 +149,6 @@ def _ipaddr_info(host, port, family, type, proto): return None -def _roundrobin(*iterables): - """roundrobin('ABC', 'D', 'EF') --> A D E B F C""" - # Copied from Python docs, Recipe credited to George Sakkis - pending = len(iterables) - nexts = itertools.cycle(iter(it).__next__ for it in iterables) - while pending: - try: - for next in nexts: - yield next() - except StopIteration: - pending -= 1 - nexts = itertools.cycle(itertools.islice(nexts, pending)) - - def _interleave_addrinfos(addrinfos, first_address_family_count=1): """Interleave list of addrinfo tuples by family.""" # Group addresses by family @@ -178,7 +164,10 @@ def _interleave_addrinfos(addrinfos, first_address_family_count=1): if first_address_family_count > 1: reordered.extend(addrinfos_lists[0][:first_address_family_count - 1]) del addrinfos_lists[0][:first_address_family_count - 1] - reordered.extend(_roundrobin(*addrinfos_lists)) + reordered.extend( + a for a in itertools.chain.from_iterable( + itertools.zip_longest(*addrinfos_lists) + ) if a is not None) return reordered From ecdc83ad24fd60a594178960b5a4c7e313cf272a Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Sun, 3 Jun 2018 16:00:21 +0800 Subject: [PATCH 14/22] Rename delay to happy_eyeballs_delay and decouple from interleave. --- Lib/asyncio/base_events.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 7587dd7a6391e5..6bac84ef749d7d 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -910,7 +910,7 @@ async def create_connection( proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, ssl_handshake_timeout=None, - delay=None, interleave=None): + happy_eyeballs_delay=None, interleave=None): """Connect to a TCP server. Create a streaming transport connection to a given Internet host and @@ -945,10 +945,7 @@ async def create_connection( raise ValueError( 'ssl_handshake_timeout is only meaningful with ssl') - if interleave is not None and delay is None: - raise ValueError('interleave is only meaningful with delay') - - if delay is not None and interleave is None: + if happy_eyeballs_delay is not None and interleave is None: # If using happy eyeballs, default to interleave addresses by family interleave = 1 @@ -973,8 +970,11 @@ async def create_connection( else: laddr_infos = None + if interleave: + infos = _interleave_addrinfos(infos, interleave) + exceptions = [] - if delay is None: + if happy_eyeballs_delay is None: # not using happy eyeballs for addrinfo in infos: try: @@ -984,13 +984,11 @@ async def create_connection( except OSError: continue else: # using happy eyeballs - if interleave: - infos = _interleave_addrinfos(infos, interleave) sock, _, _ = await staggered.staggered_race( (functools.partial(self._connect_sock, exceptions, addrinfo, laddr_infos) for addrinfo in infos), - delay, loop=self) + happy_eyeballs_delay, loop=self) if sock is None: exceptions = [exc for sub in exceptions for exc in sub] From 5fa5a9bde27cd564950fa8c52f9bd5b35c608bb3 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 5 Jun 2018 18:36:50 +0800 Subject: [PATCH 15/22] Update docs. --- Doc/library/asyncio-eventloop.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index a418674b9f1daf..45b6a20349ca10 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -340,18 +340,21 @@ Creating connections If given, these should all be integers from the corresponding :mod:`socket` module constants. - * *delay*, if given, enables Happy Eyeballs for this connection. It should + * *happy_eyeballs_delay*, if given, enables Happy Eyeballs for this + connection. It should be a floating-point number representing the amount of time in seconds to wait for a connection attempt to complete, before starting the next attempt in parallel. This is the "Connection Attempt Delay" as defined in RFC 8305. - * *interleave*, only for use together with *delay*, controls address - reordering. If ``0`` is specified, no reordering is done, and addresses are + * *interleave* controls address reordering when a host name resolves to + multiple IP addresses. + If ``0`` or unspecified, no reordering is done, and addresses are tried in the order returned by :meth:`getaddrinfo`. If a positive integer is specified, the addresses are interleaved by address family, and the given integer is interpreted as "First Address Family Count" as defined - in RFC 8305. The default is ``1``. + in RFC 8305. The default is ``0`` if *happy_eyeballs_delay* is not + specified, and ``1`` if it is. * *sock*, if given, should be an existing, already connected :class:`socket.socket` object to be used by the transport. From 321e4ac695b549a352c9f7582f9e501df151d868 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 5 Jun 2018 18:37:10 +0800 Subject: [PATCH 16/22] Update comments. --- Lib/asyncio/staggered.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/staggered.py b/Lib/asyncio/staggered.py index 5a3c223b7ac657..aadc2d39b92ae9 100644 --- a/Lib/asyncio/staggered.py +++ b/Lib/asyncio/staggered.py @@ -115,8 +115,8 @@ async def run_one_coro( # here and CancelledError are usually thrown at one, we will # encounter a curious corner case where the current task will end # up as done() == True, cancelled() == False, exception() == - # asyncio.CancelledError, which is normally not possible. - # https://bugs.python.org/issue33413 + # asyncio.CancelledError. This behavior is specified in + # https://bugs.python.org/issue30048 for i, t in enumerate(running_tasks): if i != this_index: t.cancel() From b4227ed3b5b8e4d01b7e694085fd2c3a2e6c9032 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 5 Jun 2018 18:44:20 +0800 Subject: [PATCH 17/22] Import staggered_race into main asyncio package. --- Lib/asyncio/__init__.py | 2 ++ Lib/asyncio/staggered.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Lib/asyncio/__init__.py b/Lib/asyncio/__init__.py index 26859024300e0d..07cf0c3af7e169 100644 --- a/Lib/asyncio/__init__.py +++ b/Lib/asyncio/__init__.py @@ -13,6 +13,7 @@ from .protocols import * from .runners import * from .queues import * +from .staggered import * from .streams import * from .subprocess import * from .tasks import * @@ -30,6 +31,7 @@ protocols.__all__ + runners.__all__ + queues.__all__ + + staggered.__all__ + streams.__all__ + subprocess.__all__ + tasks.__all__ + diff --git a/Lib/asyncio/staggered.py b/Lib/asyncio/staggered.py index aadc2d39b92ae9..feec681b4371bf 100644 --- a/Lib/asyncio/staggered.py +++ b/Lib/asyncio/staggered.py @@ -1,3 +1,7 @@ +"""Support for running coroutines in parallel with staggered start times.""" + +__all__ = 'staggered_race', + import contextlib import typing From cffacc7bcbd69ba1e48461d5b2115c14b4e43e8a Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 26 Jun 2018 10:16:19 +0800 Subject: [PATCH 18/22] Change `delay` to `happy_eyeballs_delay` in AbstractEventLoop. --- Lib/asyncio/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index 33f461114b7669..5ed391112e7a60 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -306,7 +306,7 @@ async def create_connection( flags=0, sock=None, local_addr=None, server_hostname=None, ssl_handshake_timeout=None, - delay=None, interleave=None): + happy_eyeballs_delay=None, interleave=None): raise NotImplementedError async def create_server( From 3e304dd470ab61d37b0ce52314fb6dd06e5422e4 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 26 Jun 2018 10:25:39 +0800 Subject: [PATCH 19/22] Change `delay` to `happy_eyeballs_delay` in documentation and blurb. --- Doc/library/asyncio-eventloop.rst | 4 ++-- .../next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 45b6a20349ca10..c94af21a29f543 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -359,7 +359,7 @@ Creating connections * *sock*, if given, should be an existing, already connected :class:`socket.socket` object to be used by the transport. If *sock* is given, none of *host*, *port*, *family*, *proto*, *flags*, - *delay*, *interleave* + *happy_eyeballs_delay*, *interleave* and *local_addr* should be specified. * *local_addr*, if given, is a ``(local_host, local_port)`` tuple used @@ -372,7 +372,7 @@ Creating connections .. versionadded:: 3.8 - The *delay* and *interleave* parameters. + The *happy_eyeballs_delay* and *interleave* parameters. .. versionadded:: 3.7 diff --git a/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst b/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst index 086d28fa770eaf..747219b1bfb899 100644 --- a/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst +++ b/Misc/NEWS.d/next/Library/2018-05-29-18-34-53.bpo-33530._4Q_bi.rst @@ -1,2 +1,3 @@ Implemented Happy Eyeballs in `asyncio.create_connection()`. Added two new -arguments, *delay* and *interleave*, to specify Happy Eyeballs behavior. +arguments, *happy_eyeballs_delay* and *interleave*, +to specify Happy Eyeballs behavior. From a8c39b9c9cc2f7d0d669429dfa726246b865f7f8 Mon Sep 17 00:00:00 2001 From: twisteroid ambassador Date: Tue, 26 Jun 2018 11:12:16 +0800 Subject: [PATCH 20/22] Add suggested value for `happy_eyeballs_delay` in documentation. --- Doc/library/asyncio-eventloop.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index c94af21a29f543..f03df99720525c 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -345,7 +345,8 @@ Creating connections be a floating-point number representing the amount of time in seconds to wait for a connection attempt to complete, before starting the next attempt in parallel. This is the "Connection Attempt Delay" as defined - in RFC 8305. + in RFC 8305. A sensible default value recommended by the RFC is 0.25 + (250 milliseconds). * *interleave* controls address reordering when a host name resolves to multiple IP addresses. From f6f2219c494ffa9d5d0442d856fd788c9d59aca4 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 1 Aug 2018 19:48:11 +0300 Subject: [PATCH 21/22] Tune RST markup --- Doc/library/asyncio-eventloop.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index f03df99720525c..e981272515ff2e 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -345,7 +345,7 @@ Creating connections be a floating-point number representing the amount of time in seconds to wait for a connection attempt to complete, before starting the next attempt in parallel. This is the "Connection Attempt Delay" as defined - in RFC 8305. A sensible default value recommended by the RFC is 0.25 + in :rfc:`8305`. A sensible default value recommended by the RFC is ``0.25`` (250 milliseconds). * *interleave* controls address reordering when a host name resolves to @@ -354,7 +354,7 @@ Creating connections tried in the order returned by :meth:`getaddrinfo`. If a positive integer is specified, the addresses are interleaved by address family, and the given integer is interpreted as "First Address Family Count" as defined - in RFC 8305. The default is ``0`` if *happy_eyeballs_delay* is not + in :rfc:`8305`. The default is ``0`` if *happy_eyeballs_delay* is not specified, and ``1`` if it is. * *sock*, if given, should be an existing, already connected From f0e37e68445c349ca0b8cfece20017f5631f02ef Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 5 May 2019 16:40:04 +0800 Subject: [PATCH 22/22] Keep .staggered as private API Do not expose .staggered submodule in __all__. Co-Authored-By: twisteroidambassador --- Lib/asyncio/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/asyncio/__init__.py b/Lib/asyncio/__init__.py index 07cf0c3af7e169..26859024300e0d 100644 --- a/Lib/asyncio/__init__.py +++ b/Lib/asyncio/__init__.py @@ -13,7 +13,6 @@ from .protocols import * from .runners import * from .queues import * -from .staggered import * from .streams import * from .subprocess import * from .tasks import * @@ -31,7 +30,6 @@ protocols.__all__ + runners.__all__ + queues.__all__ + - staggered.__all__ + streams.__all__ + subprocess.__all__ + tasks.__all__ +