Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 61b9811

Browse files
authored
gh-128552: fix refcycles in eager task creation (#128553)
1 parent 6ea04da commit 61b9811

File tree

4 files changed

+71
-6
lines changed

4 files changed

+71
-6
lines changed

Lib/asyncio/base_events.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,12 @@ def create_task(self, coro, *, name=None, context=None):
477477

478478
task.set_name(name)
479479

480-
return task
480+
try:
481+
return task
482+
finally:
483+
# gh-128552: prevent a refcycle of
484+
# task.exception().__traceback__->BaseEventLoop.create_task->task
485+
del task
481486

482487
def set_task_factory(self, factory):
483488
"""Set a task factory that will be used by loop.create_task().

Lib/asyncio/taskgroups.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,12 @@ def create_task(self, coro, *, name=None, context=None):
205205
else:
206206
self._tasks.add(task)
207207
task.add_done_callback(self._on_task_done)
208-
return task
208+
try:
209+
return task
210+
finally:
211+
# gh-128552: prevent a refcycle of
212+
# task.exception().__traceback__->TaskGroup.create_task->task
213+
del task
209214

210215
# Since Python 3.8 Tasks propagate all exceptions correctly,
211216
# except for KeyboardInterrupt and SystemExit which are

Lib/test/test_asyncio/test_taskgroups.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Adapted with permission from the EdgeDB project;
22
# license: PSFL.
33

4+
import weakref
45
import sys
56
import gc
67
import asyncio
@@ -38,7 +39,25 @@ def no_other_refs():
3839
return [coro]
3940

4041

41-
class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
42+
def set_gc_state(enabled):
43+
was_enabled = gc.isenabled()
44+
if enabled:
45+
gc.enable()
46+
else:
47+
gc.disable()
48+
return was_enabled
49+
50+
51+
@contextlib.contextmanager
52+
def disable_gc():
53+
was_enabled = set_gc_state(enabled=False)
54+
try:
55+
yield
56+
finally:
57+
set_gc_state(enabled=was_enabled)
58+
59+
60+
class BaseTestTaskGroup:
4261

4362
async def test_taskgroup_01(self):
4463

@@ -832,15 +851,15 @@ async def test_taskgroup_without_parent_task(self):
832851
with self.assertRaisesRegex(RuntimeError, "has not been entered"):
833852
tg.create_task(coro)
834853

835-
def test_coro_closed_when_tg_closed(self):
854+
async def test_coro_closed_when_tg_closed(self):
836855
async def run_coro_after_tg_closes():
837856
async with taskgroups.TaskGroup() as tg:
838857
pass
839858
coro = asyncio.sleep(0)
840859
with self.assertRaisesRegex(RuntimeError, "is finished"):
841860
tg.create_task(coro)
842-
loop = asyncio.get_event_loop()
843-
loop.run_until_complete(run_coro_after_tg_closes())
861+
862+
await run_coro_after_tg_closes()
844863

845864
async def test_cancelling_level_preserved(self):
846865
async def raise_after(t, e):
@@ -965,6 +984,30 @@ async def coro_fn():
965984
self.assertIsInstance(exc, _Done)
966985
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
967986

987+
988+
async def test_exception_refcycles_parent_task_wr(self):
989+
"""Test that TaskGroup deletes self._parent_task and create_task() deletes task"""
990+
tg = asyncio.TaskGroup()
991+
exc = None
992+
993+
class _Done(Exception):
994+
pass
995+
996+
async def coro_fn():
997+
async with tg:
998+
raise _Done
999+
1000+
with disable_gc():
1001+
try:
1002+
async with asyncio.TaskGroup() as tg2:
1003+
task_wr = weakref.ref(tg2.create_task(coro_fn()))
1004+
except* _Done as excs:
1005+
exc = excs.exceptions[0].exceptions[0]
1006+
1007+
self.assertIsNone(task_wr())
1008+
self.assertIsInstance(exc, _Done)
1009+
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
1010+
9681011
async def test_exception_refcycles_propagate_cancellation_error(self):
9691012
"""Test that TaskGroup deletes propagate_cancellation_error"""
9701013
tg = asyncio.TaskGroup()
@@ -998,5 +1041,16 @@ class MyKeyboardInterrupt(KeyboardInterrupt):
9981041
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
9991042

10001043

1044+
class TestTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
1045+
loop_factory = asyncio.EventLoop
1046+
1047+
class TestEagerTaskTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
1048+
@staticmethod
1049+
def loop_factory():
1050+
loop = asyncio.EventLoop()
1051+
loop.set_task_factory(asyncio.eager_task_factory)
1052+
return loop
1053+
1054+
10011055
if __name__ == "__main__":
10021056
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix cyclic garbage introduced by :meth:`asyncio.loop.create_task` and :meth:`asyncio.TaskGroup.create_task` holding a reference to the created task if it is eager.

0 commit comments

Comments
 (0)