-
-
Notifications
You must be signed in to change notification settings - Fork 250
Fixed child task not cancelling its task group scope when a parent scope is already cancelled #1093
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1308,6 +1308,35 @@ async def exit_scope(scope: CancelScope) -> None: | |
| ) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("anyio_backend", asyncio_params) | ||
| async def test_child_task_cancels_scope_when_parent_scope_cancelled() -> None: | ||
| """ | ||
| Regression test for #787 (weak case). | ||
|
|
||
| When a child task exits with an unhandled exception, the task group's | ||
| cancel scope must be cancelled even if an outer scope is already cancelled. | ||
| Previously the ``_effectively_cancelled`` guard in ``task_done`` prevented | ||
| this, leaving the host task unaware that a child had failed. | ||
| """ | ||
|
|
||
| async def taskfunc() -> None: | ||
| raise Exception("child task failed") | ||
|
|
||
| with pytest.raises(BaseExceptionGroup) as exc: | ||
| with CancelScope() as outer_scope: | ||
| async with create_task_group() as tg: | ||
| outer_scope.cancel() | ||
| tg.start_soon(taskfunc) | ||
| with CancelScope(shield=True): | ||
| await wait_all_tasks_blocked() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use the version of this test that uses an event (" I suppose that this point matters more for the strong case than the weak case. For the weak case, as long as we wait long enough, the test is fine and will not pass when it should fail. For the strong case, the test needs to wait until a particular event loop cycle and not longer than that.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I am foreseeing that we may remove the sleep from this test in the future in order to change it from being the weak test into the strong test. Per #787: the weak case only tests for bug (1) in #787, but the strong case tests for bug (2) also. If/when we fix bug (2), I don't see a reason to retain a separate test that still has the sleep (the weak form), because bug (1) will clearly be completely covered by the strong test already, making the weak test redundant (a waste of 0.1 s every run). In other words: if we remove # Wait at least one more scheduling round to ensure that taskfunc's
# done callback (task_done) on asyncio has finished. This is
# workaround for the delay that is currently present between a task
# failing and cancelling its task group on asyncio (#787, bug (2)).
await sleep(0.1)in the weak test would not make sense anymore, because bug (2) and the callback |
||
| await sleep(0.1) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sleep is the difference between the weak and the strong case. The reason that the sleep is here is just to work around bug (2) in #787. It might be good to have a comment here about how this test intentionally waits an extra event loop cycle for the |
||
| tg.cancel_scope.shield = True | ||
| assert tg.cancel_scope.cancel_called | ||
|
|
||
| assert len(exc.value.exceptions) == 1 | ||
| assert str(exc.value.exceptions[0]) == "child task failed" | ||
|
|
||
|
|
||
| def test_unhandled_exception_group(caplog: pytest.LogCaptureFixture) -> None: | ||
| def crash() -> NoReturn: | ||
| raise KeyboardInterrupt | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should run this on the Trio backend too: