mirror of
https://github.com/python/cpython.git
synced 2025-08-25 11:15:02 +00:00
gh-128552: fix refcycles in eager task creation (#128553)
This commit is contained in:
parent
6ea04da270
commit
61b9811ac6
4 changed files with 71 additions and 6 deletions
|
@ -477,7 +477,12 @@ class BaseEventLoop(events.AbstractEventLoop):
|
||||||
|
|
||||||
task.set_name(name)
|
task.set_name(name)
|
||||||
|
|
||||||
|
try:
|
||||||
return task
|
return task
|
||||||
|
finally:
|
||||||
|
# gh-128552: prevent a refcycle of
|
||||||
|
# task.exception().__traceback__->BaseEventLoop.create_task->task
|
||||||
|
del task
|
||||||
|
|
||||||
def set_task_factory(self, factory):
|
def set_task_factory(self, factory):
|
||||||
"""Set a task factory that will be used by loop.create_task().
|
"""Set a task factory that will be used by loop.create_task().
|
||||||
|
|
|
@ -205,7 +205,12 @@ class TaskGroup:
|
||||||
else:
|
else:
|
||||||
self._tasks.add(task)
|
self._tasks.add(task)
|
||||||
task.add_done_callback(self._on_task_done)
|
task.add_done_callback(self._on_task_done)
|
||||||
|
try:
|
||||||
return task
|
return task
|
||||||
|
finally:
|
||||||
|
# gh-128552: prevent a refcycle of
|
||||||
|
# task.exception().__traceback__->TaskGroup.create_task->task
|
||||||
|
del task
|
||||||
|
|
||||||
# Since Python 3.8 Tasks propagate all exceptions correctly,
|
# Since Python 3.8 Tasks propagate all exceptions correctly,
|
||||||
# except for KeyboardInterrupt and SystemExit which are
|
# except for KeyboardInterrupt and SystemExit which are
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Adapted with permission from the EdgeDB project;
|
# Adapted with permission from the EdgeDB project;
|
||||||
# license: PSFL.
|
# license: PSFL.
|
||||||
|
|
||||||
|
import weakref
|
||||||
import sys
|
import sys
|
||||||
import gc
|
import gc
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@ -38,7 +39,25 @@ def no_other_refs():
|
||||||
return [coro]
|
return [coro]
|
||||||
|
|
||||||
|
|
||||||
class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
|
def set_gc_state(enabled):
|
||||||
|
was_enabled = gc.isenabled()
|
||||||
|
if enabled:
|
||||||
|
gc.enable()
|
||||||
|
else:
|
||||||
|
gc.disable()
|
||||||
|
return was_enabled
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def disable_gc():
|
||||||
|
was_enabled = set_gc_state(enabled=False)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
set_gc_state(enabled=was_enabled)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTestTaskGroup:
|
||||||
|
|
||||||
async def test_taskgroup_01(self):
|
async def test_taskgroup_01(self):
|
||||||
|
|
||||||
|
@ -832,15 +851,15 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
|
||||||
with self.assertRaisesRegex(RuntimeError, "has not been entered"):
|
with self.assertRaisesRegex(RuntimeError, "has not been entered"):
|
||||||
tg.create_task(coro)
|
tg.create_task(coro)
|
||||||
|
|
||||||
def test_coro_closed_when_tg_closed(self):
|
async def test_coro_closed_when_tg_closed(self):
|
||||||
async def run_coro_after_tg_closes():
|
async def run_coro_after_tg_closes():
|
||||||
async with taskgroups.TaskGroup() as tg:
|
async with taskgroups.TaskGroup() as tg:
|
||||||
pass
|
pass
|
||||||
coro = asyncio.sleep(0)
|
coro = asyncio.sleep(0)
|
||||||
with self.assertRaisesRegex(RuntimeError, "is finished"):
|
with self.assertRaisesRegex(RuntimeError, "is finished"):
|
||||||
tg.create_task(coro)
|
tg.create_task(coro)
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(run_coro_after_tg_closes())
|
await run_coro_after_tg_closes()
|
||||||
|
|
||||||
async def test_cancelling_level_preserved(self):
|
async def test_cancelling_level_preserved(self):
|
||||||
async def raise_after(t, e):
|
async def raise_after(t, e):
|
||||||
|
@ -965,6 +984,30 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
|
||||||
self.assertIsInstance(exc, _Done)
|
self.assertIsInstance(exc, _Done)
|
||||||
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
|
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exception_refcycles_parent_task_wr(self):
|
||||||
|
"""Test that TaskGroup deletes self._parent_task and create_task() deletes task"""
|
||||||
|
tg = asyncio.TaskGroup()
|
||||||
|
exc = None
|
||||||
|
|
||||||
|
class _Done(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def coro_fn():
|
||||||
|
async with tg:
|
||||||
|
raise _Done
|
||||||
|
|
||||||
|
with disable_gc():
|
||||||
|
try:
|
||||||
|
async with asyncio.TaskGroup() as tg2:
|
||||||
|
task_wr = weakref.ref(tg2.create_task(coro_fn()))
|
||||||
|
except* _Done as excs:
|
||||||
|
exc = excs.exceptions[0].exceptions[0]
|
||||||
|
|
||||||
|
self.assertIsNone(task_wr())
|
||||||
|
self.assertIsInstance(exc, _Done)
|
||||||
|
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
|
||||||
|
|
||||||
async def test_exception_refcycles_propagate_cancellation_error(self):
|
async def test_exception_refcycles_propagate_cancellation_error(self):
|
||||||
"""Test that TaskGroup deletes propagate_cancellation_error"""
|
"""Test that TaskGroup deletes propagate_cancellation_error"""
|
||||||
tg = asyncio.TaskGroup()
|
tg = asyncio.TaskGroup()
|
||||||
|
@ -998,5 +1041,16 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
|
||||||
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
|
self.assertListEqual(gc.get_referrers(exc), no_other_refs())
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
|
||||||
|
loop_factory = asyncio.EventLoop
|
||||||
|
|
||||||
|
class TestEagerTaskTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
|
||||||
|
@staticmethod
|
||||||
|
def loop_factory():
|
||||||
|
loop = asyncio.EventLoop()
|
||||||
|
loop.set_task_factory(asyncio.eager_task_factory)
|
||||||
|
return loop
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -0,0 +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.
|
Loading…
Add table
Add a link
Reference in a new issue