Skip to content

Commit 04295d8

Browse files
committed
pythongh-117536: Fix asyncio _asyncgen_finalizer_hook()
Make the finalization of asynchronous generators more reliable. Store a strong reference to the asynchronous generator which is being closed to make sure that shutdown_asyncgens() can close it even if asyncio.run() cancels all tasks.
1 parent a2ae847 commit 04295d8

File tree

3 files changed

+44
-4
lines changed

3 files changed

+44
-4
lines changed

Lib/asyncio/base_events.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,11 @@ def __init__(self):
444444
# A weak set of all asynchronous generators that are
445445
# being iterated by the loop.
446446
self._asyncgens = weakref.WeakSet()
447+
448+
# Strong references to asynchronous generators which are being being
449+
# closed by the loop: see _asyncgen_finalizer_hook().
450+
self._close_asyncgens = set()
451+
447452
# Set to True when `loop.shutdown_asyncgens` is called.
448453
self._asyncgens_shutdown_called = False
449454
# Set to True when `loop.shutdown_default_executor` is called.
@@ -555,10 +560,22 @@ def _check_default_executor(self):
555560
if self._executor_shutdown_called:
556561
raise RuntimeError('Executor shutdown has been called')
557562

563+
async def _asyncgen_close(self, agen):
564+
await agen.aclose()
565+
self._close_asyncgens.discard(agen)
566+
558567
def _asyncgen_finalizer_hook(self, agen):
568+
if self.is_closed():
569+
self._asyncgens.discard(agen)
570+
return
571+
572+
# gh-117536: Store a strong reference to the asynchronous generator
573+
# to make sure that shutdown_asyncgens() can close it even if
574+
# asyncio.run() cancels all tasks.
575+
self._close_asyncgens.add(agen)
559576
self._asyncgens.discard(agen)
560-
if not self.is_closed():
561-
self.call_soon_threadsafe(self.create_task, agen.aclose())
577+
578+
self.call_soon_threadsafe(self.create_task, self._asyncgen_close(agen))
562579

563580
def _asyncgen_firstiter_hook(self, agen):
564581
if self._asyncgens_shutdown_called:
@@ -573,12 +590,12 @@ async def shutdown_asyncgens(self):
573590
"""Shutdown all active asynchronous generators."""
574591
self._asyncgens_shutdown_called = True
575592

576-
if not len(self._asyncgens):
593+
closing_agens = list(set(self._asyncgens) | self._close_asyncgens)
594+
if not closing_agens:
577595
# If Python version is <3.6 or we don't have any asynchronous
578596
# generators alive.
579597
return
580598

581-
closing_agens = list(self._asyncgens)
582599
self._asyncgens.clear()
583600

584601
results = await tasks.gather(

Lib/test/test_asyncio/test_base_events.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,27 @@ def test_asyncgen_finalization_by_gc_in_other_thread(self):
10441044
test_utils.run_briefly(self.loop)
10451045
self.assertTrue(status['finalized'])
10461046

1047+
def test_shutdown_asyncgens(self):
1048+
# gh-117536: Test shutdown_asyncgens() using asyncio.run() which
1049+
# may cancel the task which closes the asynchronous generator before
1050+
# calling shutdown_asyncgens().
1051+
1052+
ns = {'state': 'not started'}
1053+
async def agen():
1054+
try:
1055+
ns['state'] = 'started'
1056+
yield 0
1057+
yield 1
1058+
finally:
1059+
ns['state'] = 'finalized'
1060+
1061+
async def reproducer():
1062+
async for item in agen():
1063+
break
1064+
1065+
asyncio.run(reproducer())
1066+
self.assertEqual(ns['state'], 'finalized')
1067+
10471068

10481069
class MyProto(asyncio.Protocol):
10491070
done = None
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`asyncio`: Make the finalization of asynchronous generators more
2+
reliable. Patch by Victor Stinner.

0 commit comments

Comments
 (0)