Skip to content

Commit 594c369

Browse files
gvanrossumambv
andauthored
GH-94398: TaskGroup: Fail create_task() during shutdown (GH-94400)
Once the task group is shutting down, it should not be possible to create a new task. Here "shutting down" means `self._aborting` is set, indicating that at least one task has failed and we have cancelled all others. Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent 4261b6b commit 594c369

File tree

4 files changed

+27
-4
lines changed

4 files changed

+27
-4
lines changed

Doc/library/asyncio-task.rst

+1
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ no new tasks may be added to the group.
320320
The first time any of the tasks belonging to the group fails
321321
with an exception other than :exc:`asyncio.CancelledError`,
322322
the remaining tasks in the group are cancelled.
323+
No further tasks can then be added to the group.
323324
At this point, if the body of the ``async with`` statement is still active
324325
(i.e., :meth:`~object.__aexit__` hasn't been called yet),
325326
the task directly containing the ``async with`` statement is also cancelled.

Lib/asyncio/taskgroups.py

+2
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ def create_task(self, coro, *, name=None, context=None):
138138
raise RuntimeError(f"TaskGroup {self!r} has not been entered")
139139
if self._exiting and not self._tasks:
140140
raise RuntimeError(f"TaskGroup {self!r} is finished")
141+
if self._aborting:
142+
raise RuntimeError(f"TaskGroup {self!r} is shutting down")
141143
if context is None:
142144
task = self._loop.create_task(coro)
143145
else:

Lib/test/test_asyncio/test_taskgroups.py

+23-4
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,8 @@ async def runner():
122122
self.assertTrue(t2.cancelled())
123123

124124
async def test_cancel_children_on_child_error(self):
125-
"""
126-
When a child task raises an error, the rest of the children
127-
are cancelled and the errors are gathered into an EG.
128-
"""
125+
# When a child task raises an error, the rest of the children
126+
# are cancelled and the errors are gathered into an EG.
129127

130128
NUM = 0
131129
t2_cancel = False
@@ -722,6 +720,27 @@ async def coro(val):
722720
await t2
723721
self.assertEqual(2, ctx.get(cvar))
724722

723+
async def test_taskgroup_no_create_task_after_failure(self):
724+
async def coro1():
725+
await asyncio.sleep(0.001)
726+
1 / 0
727+
async def coro2(g):
728+
try:
729+
await asyncio.sleep(1)
730+
except asyncio.CancelledError:
731+
with self.assertRaises(RuntimeError):
732+
g.create_task(c1 := coro1())
733+
# We still have to await c1 to avoid a warning
734+
with self.assertRaises(ZeroDivisionError):
735+
await c1
736+
737+
with self.assertRaises(ExceptionGroup) as cm:
738+
async with taskgroups.TaskGroup() as g:
739+
g.create_task(coro1())
740+
g.create_task(coro2(g))
741+
742+
self.assertEqual(get_error_types(cm.exception), {ZeroDivisionError})
743+
725744

726745
if __name__ == "__main__":
727746
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Once a :class:`asyncio.TaskGroup` has started shutting down (i.e., at least one task has failed and the task group has started cancelling the remaining tasks), it should not be possible to add new tasks to the task group.

0 commit comments

Comments
 (0)