8000 [3.13] gh-128552: fix refcycles in eager task creation (#128553) (#12… · python/cpython@1383588 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1383588

Browse files
authored
[3.13] gh-128552: fix refcycles in eager task creation (#128553) (#128585)
gh-128552: fix refcycles in eager task creation (#128553) (cherry picked from commit 61b9811)
1 parent 7e099c5 commit 1383588

File tree

4 files changed

+72
-6
lines changed

4 files changed

+72
-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: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Adapted with permission from the EdgeDB project;
22
# license: PSFL.
33

4+
import weakref
5+
import sys
46
import gc
57
import asyncio
68
import contextvars
@@ -28,7 +30,25 @@ def get_error_types(eg):
2830
return {type(exc) for exc in eg.exceptions}
2931

3032

31-
class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
33+
def set_gc_state(enabled):
34+
was_enabled = gc.isenabled()
35+
if enabled:
36+
gc.enable()
37+
else:
38+
gc.disable()
39+
return was_enabled
40+
41+
42+
@contextlib.contextmanager
43+
def disable_gc():
44+
was_enabled = set_gc_state(enabled=False)
45+
try:
46+
yield
47+
finally:
48+
set_gc_state(enabled=was_enabled)
49+
50+
51+
class BaseTestTaskGroup:
3252

3353
async def test_taskgroup_01(self):
3454

@@ -822,15 +842,15 @@ async def test_taskgroup_without_parent_task(self):
822842
with self.assertRaisesRegex(RuntimeError, "has not been entered"):
823843
tg.create_task(coro)
824844

825-
def test_coro_closed_when_tg_closed(self):
845+
async def test_coro_closed_when_tg_closed(self):
826846
async def run_coro_after_tg_closes():
827847
async with taskgroups.TaskGroup() as tg:
828848
pass
829849
coro = asyncio.sleep(0)
830850
with self.assertRaisesRegex(RuntimeError, "is finished"):
831851
tg.create_task(coro)
832-
loop = asyncio.get_event_loop()
833-
loop.run_until_complete(run_coro_after_tg_closes())
852+
853+
await run_coro_after_tg_closes()
834854

835855
async def test_cancelling_level_preserved(self):
836856
async def raise_after(t, e):
@@ -955,6 +975,30 @@ async def coro_fn():
955975
self.assertIsInstance(exc, _Done)
956976
self.assertListEqual(gc.get_referrers(exc), [])
957977

978+
979+
async def test_exception_refcycles_parent_task_wr(self):
980+
"""Test that TaskGroup deletes self._parent_task and create_task() deletes task"""
981+
tg = asyncio.TaskGroup()
982+
exc = None
983+
984+
class _Done(Exception):
985+
pass
986+
987+
async def coro_fn():
988+
async with tg:
989+
raise _Done
990+
991+
with disable_gc():
992+
try:
993+
async with asyncio.TaskGroup() as tg2:
994+
task_wr = weakref.ref(tg2.create_task(coro_fn()))
995+
except* _Done as excs:
996+
exc = excs.exceptions[0].exceptions[0]
997+
998+
self.assertIsNone(task_wr())
999+
self.assertIsInstance(exc, _Done)
1000+
self.assertListEqual(gc.get_referrers(exc), [])
1001+
9581002
async def test_exception_refcycles_propagate_cancellation_error(self):
9591003
"""Test that TaskGroup deletes propagate_cancellation_error"""
9601004
tg = asyncio.TaskGroup()
@@ -988,5 +1032,16 @@ class MyKeyboardInterrupt(KeyboardInterrupt):
9881032
self.assertListEqual(gc.get_referrers(exc), [])
9891033

9901034

1035+
class TestTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
1036+
loop_factory = asyncio.EventLoop
1037+
1038+
class TestEagerTaskTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
1039+
@staticmethod
1040+
def loop_factory():
1041+
loop = asyncio.EventLoop()
1042+
loop.set_task_factory(asyncio.eager_task_factory)
1043+
return loop
1044+
1045+
9911046
if __name__ == "__main__":
9921047
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)
0