mirror of
https://github.com/python/cpython.git
synced 2025-08-09 11:29:45 +00:00
[3.12] gh-128588: fix refcycles in eager task creation and remove eager tasks optimization that missed and introduced incorrect cancellations (#129063) (#128586)
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
This commit is contained in:
parent
23cb53a312
commit
bc214545f9
5 changed files with 141 additions and 10 deletions
|
@ -466,7 +466,12 @@ class BaseEventLoop(events.AbstractEventLoop):
|
||||||
|
|
||||||
tasks._set_task_name(task, name)
|
tasks._set_task_name(task, 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().
|
||||||
|
|
|
@ -185,15 +185,20 @@ class TaskGroup:
|
||||||
else:
|
else:
|
||||||
task = self._loop.create_task(coro, context=context)
|
task = self._loop.create_task(coro, context=context)
|
||||||
tasks._set_task_name(task, name)
|
tasks._set_task_name(task, name)
|
||||||
# optimization: Immediately call the done callback if the task is
|
|
||||||
|
# Always schedule the done callback even if the task is
|
||||||
# already done (e.g. if the coro was able to complete eagerly),
|
# already done (e.g. if the coro was able to complete eagerly),
|
||||||
# and skip scheduling a done callback
|
# otherwise if the task completes with an exception then it will cancel
|
||||||
if task.done():
|
# the current task too early. gh-128550, gh-128588
|
||||||
self._on_task_done(task)
|
|
||||||
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,8 @@
|
||||||
# Adapted with permission from the EdgeDB project;
|
# Adapted with permission from the EdgeDB project;
|
||||||
# license: PSFL.
|
# license: PSFL.
|
||||||
|
|
||||||
|
import weakref
|
||||||
|
import sys
|
||||||
import gc
|
import gc
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextvars
|
import contextvars
|
||||||
|
@ -27,7 +29,25 @@ def get_error_types(eg):
|
||||||
return {type(exc) for exc in eg.exceptions}
|
return {type(exc) for exc in eg.exceptions}
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
|
|
||||||
|
@ -880,6 +900,30 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
|
||||||
self.assertIsInstance(exc, _Done)
|
self.assertIsInstance(exc, _Done)
|
||||||
self.assertListEqual(gc.get_referrers(exc), [])
|
self.assertListEqual(gc.get_referrers(exc), [])
|
||||||
|
|
||||||
|
|
||||||
|
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), [])
|
||||||
|
|
||||||
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()
|
||||||
|
@ -912,6 +956,81 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
|
||||||
self.assertIsNotNone(exc)
|
self.assertIsNotNone(exc)
|
||||||
self.assertListEqual(gc.get_referrers(exc), [])
|
self.assertListEqual(gc.get_referrers(exc), [])
|
||||||
|
|
||||||
|
async def test_cancels_task_if_created_during_creation(self):
|
||||||
|
# regression test for gh-128550
|
||||||
|
ran = False
|
||||||
|
class MyError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
exc = None
|
||||||
|
try:
|
||||||
|
async with asyncio.TaskGroup() as tg:
|
||||||
|
async def third_task():
|
||||||
|
raise MyError("third task failed")
|
||||||
|
|
||||||
|
async def second_task():
|
||||||
|
nonlocal ran
|
||||||
|
tg.create_task(third_task())
|
||||||
|
with self.assertRaises(asyncio.CancelledError):
|
||||||
|
await asyncio.sleep(0) # eager tasks cancel here
|
||||||
|
await asyncio.sleep(0) # lazy tasks cancel here
|
||||||
|
ran = True
|
||||||
|
|
||||||
|
tg.create_task(second_task())
|
||||||
|
except* MyError as excs:
|
||||||
|
exc = excs.exceptions[0]
|
||||||
|
|
||||||
|
self.assertTrue(ran)
|
||||||
|
self.assertIsInstance(exc, MyError)
|
||||||
|
|
||||||
|
async def test_cancellation_does_not_leak_out_of_tg(self):
|
||||||
|
class MyError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def throw_error():
|
||||||
|
raise MyError
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with asyncio.TaskGroup() as tg:
|
||||||
|
tg.create_task(throw_error())
|
||||||
|
except* MyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.fail("should have raised one MyError in group")
|
||||||
|
|
||||||
|
# if this test fails this current task will be cancelled
|
||||||
|
# outside the task group and inside unittest internals
|
||||||
|
# we yield to the event loop with sleep(0) so that
|
||||||
|
# cancellation happens here and error is more understandable
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
EventLoop = asyncio.ProactorEventLoop
|
||||||
|
else:
|
||||||
|
EventLoop = asyncio.SelectorEventLoop
|
||||||
|
|
||||||
|
|
||||||
|
class IsolatedAsyncioTestCase(unittest.IsolatedAsyncioTestCase):
|
||||||
|
loop_factory = None
|
||||||
|
|
||||||
|
def _setupAsyncioRunner(self):
|
||||||
|
assert self._asyncioRunner is None, 'asyncio runner is already initialized'
|
||||||
|
runner = asyncio.Runner(debug=True, loop_factory=self.loop_factory)
|
||||||
|
self._asyncioRunner = runner
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskGroup(BaseTestTaskGroup, IsolatedAsyncioTestCase):
|
||||||
|
loop_factory = EventLoop
|
||||||
|
|
||||||
|
|
||||||
|
class TestEagerTaskTaskGroup(BaseTestTaskGroup, IsolatedAsyncioTestCase):
|
||||||
|
@staticmethod
|
||||||
|
def loop_factory():
|
||||||
|
loop = 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.
|
|
@ -0,0 +1 @@
|
||||||
|
Removed an incorrect optimization relating to eager tasks in :class:`asyncio.TaskGroup` that resulted in cancellations being missed.
|
Loading…
Add table
Add a link
Reference in a new issue