Skip to content

Commit bffed1a

Browse files
committed
gen: Hold strong references to all asyncio.Tasks
Per the warning in the asyncio documentation, we need to hold a strong reference to all asyncio Tasks to prevent premature GC. Following discussions in cpython (python/cpython#91887), we hold these references on the IOLoop instance to ensure that they are strongly held but do not cause leaks if the event loop itself is discarded. This is expected to fix all of the various "task was destroyed but it is pending" warnings that have been reported. The IOLoop._pending_tasks set is expected to become obsolete if corresponding changes are made to asyncio in Python 3.13. Fixes tornadoweb#3209 Fixes tornadoweb#3047 Fixes tornadoweb#2763 Some issues involve this warning as their most visible symptom, but have an underlying cause that should still be addressed. Updates tornadoweb#2914 Updates tornadoweb#2356
1 parent f41d78d commit bffed1a

File tree

2 files changed

+30
-8
lines changed

2 files changed

+30
-8
lines changed

tornado/gen.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -840,13 +840,17 @@ def handle_exception(
840840
return False
841841

842842

843-
# Convert Awaitables into Futures.
844-
try:
845-
_wrap_awaitable = asyncio.ensure_future
846-
except AttributeError:
847-
# asyncio.ensure_future was introduced in Python 3.4.4, but
848-
# Debian jessie still ships with 3.4.2 so try the old name.
849-
_wrap_awaitable = getattr(asyncio, "async")
843+
def _wrap_awaitable(awaitable: Awaitable) -> Future:
844+
# Convert Awaitables into Futures.
845+
# Note that we use ensure_future, which handles both awaitables
846+
# and coroutines, rather than create_task, which only accepts
847+
# coroutines. (ensure_future calls create_task if given a coroutine)
848+
fut = asyncio.ensure_future(awaitable)
849+
# See comments on IOLoop._pending_tasks.
850+
loop = IOLoop.current()
851+
loop._register_task(fut)
852+
fut.add_done_callback(lambda f: loop._unregister_task(f))
853+
return fut
850854

851855

852856
def convert_yielded(yielded: _Yieldable) -> Future:

tornado/ioloop.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
from typing import Union, Any, Type, Optional, Callable, TypeVar, Tuple, Awaitable
5151

5252
if typing.TYPE_CHECKING:
53-
from typing import Dict, List # noqa: F401
53+
from typing import Dict, List, Set # noqa: F401
5454

5555
from typing_extensions import Protocol
5656
else:
@@ -159,6 +159,18 @@ async def main():
159159
# In Python 3, _ioloop_for_asyncio maps from asyncio loops to IOLoops.
160160
_ioloop_for_asyncio = dict() # type: Dict[asyncio.AbstractEventLoop, IOLoop]
161161

162+
# Maintain a set of all pending tasks to follow the warning in the docs
163+
# of asyncio.create_tasks:
164+
# https://docs.python.org/3.11/library/asyncio-task.html#asyncio.create_task
165+
# This ensures that all pending tasks have a strong reference so they
166+
# will not be garbage collected before they are finished.
167+
# (Thus avoiding "task was destroyed but it is pending" warnings)
168+
# An analogous change has been proposed in cpython for 3.13:
169+
# https://github.com/python/cpython/issues/91887
170+
# If that change is accepted, this can eventually be removed.
171+
# If it is not, we will consider the rationale and may remove this.
172+
_pending_tasks = set() # type: Set[Future]
173+
162174
@classmethod
163175
def configure(
164176
cls, impl: "Union[None, str, Type[Configurable]]", **kwargs: Any
@@ -805,6 +817,12 @@ def close_fd(self, fd: Union[int, _Selectable]) -> None:
805817
except OSError:
806818
pass
807819

820+
def _register_task(self, f: Future) -> None:
821+
self._pending_tasks.add(f)
822+
823+
def _unregister_task(self, f: Future) -> None:
824+
self._pending_tasks.discard(f)
825+
808826

809827
class _Timeout(object):
810828
"""An IOLoop timeout, a UNIX timestamp and a callback"""

0 commit comments

Comments
 (0)