GH-95704: Don't suppress errors from tasks when TG is cancelled (#95761)

When a task catches CancelledError and raises some other error,
the other error should not silently be suppressed.

Any scenario where a task crashes in cleanup upon cancellation
will now result in an ExceptionGroup wrapping the crash(es)
instead of propagating CancelledError and ignoring the side errors.

NOTE: This represents a change in behavior (hence the need to
change several tests).  But it is only an edge case.

Co-authored-by: Thomas Grainger <tagrain@gmail.com>
This commit is contained in:
Guido van Rossum 2022-08-16 18:23:06 -07:00 committed by GitHub
parent 9b30b965f0
commit f51f54f39d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 35 additions and 28 deletions

View file

@ -116,10 +116,9 @@ class TaskGroup:
if self._base_error is not None: if self._base_error is not None:
raise self._base_error raise self._base_error
if propagate_cancellation_error is not None: # Propagate CancelledError if there is one, except if there
# The wrapping task was cancelled; since we're done with # are other errors -- those have priority.
# closing all child tasks, just propagate the cancellation if propagate_cancellation_error and not self._errors:
# request now.
raise propagate_cancellation_error raise propagate_cancellation_error
if et is not None and et is not exceptions.CancelledError: if et is not None and et is not exceptions.CancelledError:

View file

@ -230,10 +230,12 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
self.assertEqual(NUM, 15) self.assertEqual(NUM, 15)
async def test_cancellation_in_body(self): async def test_taskgroup_08(self):
async def foo(): async def foo():
await asyncio.sleep(0.1) try:
await asyncio.sleep(10)
finally:
1 / 0 1 / 0
async def runner(): async def runner():
@ -241,18 +243,16 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
for _ in range(5): for _ in range(5):
g.create_task(foo()) g.create_task(foo())
try:
await asyncio.sleep(10) await asyncio.sleep(10)
except asyncio.CancelledError:
raise
r = asyncio.create_task(runner()) r = asyncio.create_task(runner())
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
self.assertFalse(r.done()) self.assertFalse(r.done())
r.cancel() r.cancel()
with self.assertRaises(asyncio.CancelledError) as cm: with self.assertRaises(ExceptionGroup) as cm:
await r await r
self.assertEqual(get_error_types(cm.exception), {ZeroDivisionError})
async def test_taskgroup_09(self): async def test_taskgroup_09(self):
@ -316,7 +316,9 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
async def test_taskgroup_11(self): async def test_taskgroup_11(self):
async def foo(): async def foo():
await asyncio.sleep(0.1) try:
await asyncio.sleep(10)
finally:
1 / 0 1 / 0
async def runner(): async def runner():
@ -325,23 +327,25 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
for _ in range(5): for _ in range(5):
g2.create_task(foo()) g2.create_task(foo())
try:
await asyncio.sleep(10) await asyncio.sleep(10)
except asyncio.CancelledError:
raise
r = asyncio.create_task(runner()) r = asyncio.create_task(runner())
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
self.assertFalse(r.done()) self.assertFalse(r.done())
r.cancel() r.cancel()
with self.assertRaises(asyncio.CancelledError): with self.assertRaises(ExceptionGroup) as cm:
await r await r
self.assertEqual(get_error_types(cm.exception), {ExceptionGroup})
self.assertEqual(get_error_types(cm.exception.exceptions[0]), {ZeroDivisionError})
async def test_taskgroup_12(self): async def test_taskgroup_12(self):
async def foo(): async def foo():
await asyncio.sleep(0.1) try:
await asyncio.sleep(10)
finally:
1 / 0 1 / 0
async def runner(): async def runner():
@ -352,19 +356,19 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
for _ in range(5): for _ in range(5):
g2.create_task(foo()) g2.create_task(foo())
try:
await asyncio.sleep(10) await asyncio.sleep(10)
except asyncio.CancelledError:
raise
r = asyncio.create_task(runner()) r = asyncio.create_task(runner())
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
self.assertFalse(r.done()) self.assertFalse(r.done())
r.cancel() r.cancel()
with self.assertRaises(asyncio.CancelledError): with self.assertRaises(ExceptionGroup) as cm:
await r await r
self.assertEqual(get_error_types(cm.exception), {ExceptionGroup})
self.assertEqual(get_error_types(cm.exception.exceptions[0]), {ZeroDivisionError})
async def test_taskgroup_13(self): async def test_taskgroup_13(self):
async def crash_after(t): async def crash_after(t):
@ -424,8 +428,9 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
self.assertFalse(r.done()) self.assertFalse(r.done())
r.cancel() r.cancel()
with self.assertRaises(asyncio.CancelledError): with self.assertRaises(ExceptionGroup) as cm:
await r await r
self.assertEqual(get_error_types(cm.exception), {ZeroDivisionError})
async def test_taskgroup_16(self): async def test_taskgroup_16(self):
@ -451,8 +456,9 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase):
self.assertFalse(r.done()) self.assertFalse(r.done())
r.cancel() r.cancel()
with self.assertRaises(asyncio.CancelledError): with self.assertRaises(ExceptionGroup) as cm:
await r await r
self.assertEqual(get_error_types(cm.exception), {ZeroDivisionError})
async def test_taskgroup_17(self): async def test_taskgroup_17(self):
NUM = 0 NUM = 0

View file

@ -0,0 +1,2 @@
When a task catches :exc:`asyncio.CancelledError` and raises some other error,
the other error should generally not silently be suppressed.