Skip to content

Commit 0051cd9

Browse files
bdowningachimnol
andauthored
Fix "unexpected cancel" when parent is cancelled but needs to continue (#35)
resolves #34. * When the async-with block (i.e., the parent task) is cancelled due to an unhandled exception from child tasks, `__aexit__()` sees the cancellation error instead of the source exception that triggered it. This should be wrapped as ExceptionGroup and the parent cancellation should be undone. * The prior implementation from EdgeDB has been catching this parent cancellation as a full task cancellation, rather than a transient signal to exit the async-with block only. * Fix this issue by taking the implementation of `_is_base_error()` from Python 3.11's `asyncio.TaskGroup` instead of changing `__aexit__()` method. * Use more specific exception class in the test case to prevent unintended catch-all situation in terms of regression check. Co-authored-by: Joongi Kim <joongi@lablup.com>
1 parent 02be382 commit 0051cd9

File tree

3 files changed

+28
-1
lines changed

3 files changed

+28
-1
lines changed

changes/35.feature.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix "unexpected cancel" bug in `TaskGroup`.

src/aiotools/taskgroup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def create_task(self, coro):
211211

212212
def _is_base_error(self, exc):
213213
assert isinstance(exc, BaseException)
214-
return not isinstance(exc, Exception)
214+
return isinstance(exc, (SystemExit, KeyboardInterrupt))
215215

216216
def _patch_task(self, task):
217217
# In Python 3.8 we'll need proper API on asyncio.Task to

tests/test_taskgroup.py

+26
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,32 @@ async def parent():
186186
assert results == []
187187

188188

189+
@pytest.mark.asyncio
190+
async def test_taskgroup_distinguish_inner_error_and_outer_cancel():
191+
192+
async def do_error():
193+
await asyncio.sleep(0.5)
194+
raise ValueError("bad stuff")
195+
196+
with VirtualClock().patch_loop():
197+
198+
with pytest.raises(TaskGroupError) as eg:
199+
async with TaskGroup() as tg:
200+
t1 = tg.create_task(do_error())
201+
# The following sleep is cancelled due to do_error(),
202+
# raising CancelledError!
203+
await asyncio.sleep(1)
204+
# We need to preserve the source exception instead of the outer
205+
# cancellation. __aexit__() should not treat CancelledError as a
206+
# base error to handle such cases.
207+
208+
assert t1.done()
209+
assert not t1.cancelled()
210+
assert isinstance(t1.exception(), ValueError)
211+
assert len(eg.value.__errors__) == 1
212+
assert isinstance(eg.value.__errors__[0], ValueError)
213+
214+
189215
@pytest.mark.asyncio
190216
async def test_taskgroup_error():
191217
with VirtualClock().patch_loop():

0 commit comments

Comments
 (0)