Skip to content

Commit a7b2e1f

Browse files
gh-135736: call exception handler for all BaseExceptions in asyncio.TaskGroup (#154538)
1 parent 6611f4d commit a7b2e1f

3 files changed

Lines changed: 50 additions & 0 deletions

File tree

Lib/asyncio/taskgroups.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,18 @@ async def _aexit(self, et, exc):
140140
assert not self._tasks
141141

142142
if self._base_error is not None:
143+
# self._base_error (SystemExit or KeyboardInterrupt) is about
144+
# to propagate out of this method, which discards any other
145+
# collected task errors silently. Report them instead of
146+
# losing them. See gh-135736.
147+
for suppressed_exc in self._errors:
148+
self._loop.call_exception_handler({
149+
'message': 'TaskGroup task exception was not '
150+
'propagated because the TaskGroup body '
151+
'is being closed with a BaseException',
152+
'exception': suppressed_exc,
153+
'task_group': self,
154+
})
143155
try:
144156
raise self._base_error
145157
finally:

Lib/test/test_asyncio/test_taskgroups.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,39 @@ async def runner():
608608
get_error_types(cm.exception), {MyBaseExc, ZeroDivisionError}
609609
)
610610

611+
async def test_taskgroup_20b(self):
612+
# Same setup as test_taskgroup_20 (a KeyboardInterrupt from the
613+
# "async with" body itself, alongside a sibling task's exception):
614+
# raising self._base_error out of _aexit() discards self._errors
615+
# silently. The sibling's exception can't be raised alongside
616+
# the KeyboardInterrupt (only one exception can propagate), but
617+
# it must be reported via the loop's exception handler instead
618+
# of being discarded silently. See gh-135736.
619+
async def crash_soon():
620+
await asyncio.sleep(0.1)
621+
1 / 0
622+
623+
async def nested():
624+
try:
625+
await asyncio.sleep(10)
626+
finally:
627+
raise KeyboardInterrupt
628+
629+
async def runner():
630+
async with taskgroups.TaskGroup() as g:
631+
g.create_task(crash_soon())
632+
await nested()
633+
634+
contexts = []
635+
loop = asyncio.get_running_loop()
636+
loop.set_exception_handler(lambda loop, context: contexts.append(context))
637+
638+
with self.assertRaises(KeyboardInterrupt):
639+
await runner()
640+
641+
self.assertEqual(len(contexts), 1)
642+
self.assertIsInstance(contexts[0]['exception'], ZeroDivisionError)
643+
611644
async def _test_taskgroup_21(self):
612645
# This test doesn't work as asyncio, currently, doesn't
613646
# correctly propagate KeyboardInterrupt (or SystemExit) --
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :class:`asyncio.TaskGroup` silently discarding errors from sibling
2+
tasks whenever the ``async with`` block exits with a :exc:`SystemExit` or
3+
:exc:`KeyboardInterrupt`. These errors are now reported via
4+
:meth:`loop.call_exception_handler() <asyncio.loop.call_exception_handler>`
5+
instead of being lost.

0 commit comments

Comments
 (0)