Skip to content

[3.11] GH-94398: TaskGroup: Fail create_task() during shutdown (GH-94400) #94463

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Doc/library/asyncio-task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ no new tasks may be added to the group.
The first time any of the tasks belonging to the group fails
with an exception other than :exc:`asyncio.CancelledError`,
the remaining tasks in the group are cancelled.
No further tasks can then be added to the group.
At this point, if the body of the ``async with`` statement is still active
(i.e., :meth:`~object.__aexit__` hasn't been called yet),
the task directly containing the ``async with`` statement is also cancelled.
Expand Down
2 changes: 2 additions & 0 deletions Lib/asyncio/taskgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ def create_task(self, coro, *, name=None, context=None):
raise RuntimeError(f"TaskGroup {self!r} has not been entered")
if self._exiting and not self._tasks:
raise RuntimeError(f"TaskGroup {self!r} is finished")
if self._aborting:
raise RuntimeError(f"TaskGroup {self!r} is shutting down")
if context is None:
task = self._loop.create_task(coro)
else:
Expand Down
27 changes: 23 additions & 4 deletions Lib/test/test_asyncio/test_taskgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,8 @@ async def runner():
self.assertTrue(t2.cancelled())

async def test_cancel_children_on_child_error(self):
"""
When a child task raises an error, the rest of the children
are cancelled and the errors are gathered into an EG.
"""
# When a child task raises an error, the rest of the children
# are cancelled and the errors are gathered into an EG.

NUM = 0
t2_cancel = False
Expand Down Expand Up @@ -722,6 +720,27 @@ async def coro(val):
await t2
self.assertEqual(2, ctx.get(cvar))

async def test_taskgroup_no_create_task_after_failure(self):
async def coro1():
await asyncio.sleep(0.001)
1 / 0
async def coro2(g):
try:
await asyncio.sleep(1)
except asyncio.CancelledError:
with self.assertRaises(RuntimeError):
g.create_task(c1 := coro1())
# We still have to await c1 to avoid a warning
with self.assertRaises(ZeroDivisionError):
await c1

with self.assertRaises(ExceptionGroup) as cm:
async with taskgroups.TaskGroup() as g:
g.create_task(coro1())
g.create_task(coro2(g))

self.assertEqual(get_error_types(cm.exception), {ZeroDivisionError})


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +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.