-
-
Notifications
You must be signed in to change notification settings - Fork 343
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
Nursery: don't act as a checkpoint when not running anything #1696
Changes from 6 commits
5c7e5ef
a3d9d25
3641411
e66c5a5
85f61dd
4b1c815
3907400
27ac88a
06c176c
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 |
---|---|---|
@@ -0,0 +1,7 @@ | ||
When exiting a nursery block, the parent task always waits for child | ||
tasks to exit. This wait cannot be cancelled. However, previously, if | ||
you tried to cancel it, it *would* inject a `Cancelled` exception, | ||
even though it wasn't cancelled. Most users probably never noticed | ||
either way, but injecting a `Cancelled` here is not really useful, and | ||
in some rare cases caused confusion or problems, so Trio no longer | ||
does that. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -433,12 +433,12 @@ async def crasher(): | |
# nursery block exited, all cancellations inside the | ||
# nursery block continue propagating to reach the | ||
# outer scope. | ||
assert len(multi_exc.exceptions) == 5 | ||
assert len(multi_exc.exceptions) == 4 | ||
summary = {} | ||
for exc in multi_exc.exceptions: | ||
summary.setdefault(type(exc), 0) | ||
summary[type(exc)] += 1 | ||
assert summary == {_core.Cancelled: 4, KeyError: 1} | ||
assert summary == {_core.Cancelled: 3, KeyError: 1} | ||
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. Clarifying the cancelled count in a comment might be clearer here for those unfamiliar with the recent change? Something like we only receive cancelleds from subtasks not from the nursery itself? |
||
raise | ||
except AssertionError: # pragma: no cover | ||
raise | ||
|
@@ -1101,6 +1101,9 @@ async def child(): | |
assert isinstance(excinfo.value.__context__, KeyError) | ||
|
||
|
||
@pytest.mark.skipif( | ||
sys.version_info < (3, 6, 2), reason="https://bugs.python.org/issue29600" | ||
) | ||
async def test_nursery_exception_chaining_doesnt_make_context_loops(): | ||
async def crasher(): | ||
raise KeyError | ||
|
@@ -1605,20 +1608,35 @@ async def test_trivial_yields(): | |
await _core.checkpoint_if_cancelled() | ||
await _core.cancel_shielded_checkpoint() | ||
|
||
with assert_checkpoints(): | ||
# Weird case: opening and closing a nursery schedules, but doesn't check | ||
# for cancellation (unless something inside the nursery does) | ||
task = _core.current_task() | ||
before_schedule_points = task._schedule_points | ||
with _core.CancelScope() as cs: | ||
cs.cancel() | ||
async with _core.open_nursery(): | ||
pass | ||
assert not cs.cancelled_caught | ||
assert task._schedule_points > before_schedule_points | ||
|
||
before_schedule_points = task._schedule_points | ||
|
||
async def noop_with_no_checkpoint(): | ||
pass | ||
|
||
with _core.CancelScope() as cs: | ||
cs.cancel() | ||
async with _core.open_nursery() as nursery: | ||
nursery.start_soon(noop_with_no_checkpoint) | ||
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 presume it's out of scope (and already covered in other tests) but normally doing this cancellation before opening the nursery results in cancellation at the first checkpoint right? |
||
assert not cs.cancelled_caught | ||
|
||
assert task._schedule_points > before_schedule_points | ||
|
||
with _core.CancelScope() as cancel_scope: | ||
cancel_scope.cancel() | ||
with pytest.raises(_core.MultiError) as excinfo: | ||
with pytest.raises(KeyError): | ||
async with _core.open_nursery(): | ||
raise KeyError | ||
assert len(excinfo.value.exceptions) == 2 | ||
assert {type(e) for e in excinfo.value.exceptions} == { | ||
KeyError, | ||
_core.Cancelled, | ||
} | ||
|
||
|
||
async def test_nursery_start(autojump_clock): | ||
|
@@ -1685,28 +1703,37 @@ async def nothing(task_status=_core.TASK_STATUS_IGNORED): | |
# is ignored; start() raises Cancelled. | ||
async def just_started(task_status=_core.TASK_STATUS_IGNORED): | ||
task_status.started("hi") | ||
await _core.checkpoint() | ||
|
||
async with _core.open_nursery() as nursery: | ||
with _core.CancelScope() as cs: | ||
cs.cancel() | ||
with pytest.raises(_core.Cancelled): | ||
await nursery.start(just_started) | ||
|
||
# and if after the no-op started(), the child crashes, the error comes out | ||
# of start() | ||
# but if the task does not execute any checkpoints, and exits, then start() | ||
# doesn't raise Cancelled, since the task completed successfully. | ||
async def started_with_no_checkpoint(task_status=_core.TASK_STATUS_IGNORED): | ||
task_status.started("hi") | ||
|
||
async with _core.open_nursery() as nursery: | ||
with _core.CancelScope() as cs: | ||
cs.cancel() | ||
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. In the case where you cancel from inside the nursery after having started tasks with checkpoints, won't counts of expected cancels change as well or is that entirely covered by the one test change in |
||
await nursery.start(started_with_no_checkpoint) | ||
assert not cs.cancelled_caught | ||
|
||
# and since starting in a cancelled context makes started() a no-op, if | ||
# the child crashes after calling started(), the error can *still* come | ||
# out of start() | ||
async def raise_keyerror_after_started(task_status=_core.TASK_STATUS_IGNORED): | ||
task_status.started() | ||
raise KeyError("whoopsiedaisy") | ||
|
||
async with _core.open_nursery() as nursery: | ||
with _core.CancelScope() as cs: | ||
cs.cancel() | ||
with pytest.raises(_core.MultiError) as excinfo: | ||
with pytest.raises(KeyError): | ||
await nursery.start(raise_keyerror_after_started) | ||
assert {type(e) for e in excinfo.value.exceptions} == { | ||
_core.Cancelled, | ||
KeyError, | ||
} | ||
|
||
# trying to start in a closed nursery raises an error immediately | ||
async with _core.open_nursery() as closed_nursery: | ||
|
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.
So this means the
Cancelled
is still injected but now is ignored via the newisinstance
check?Also if the following is true:
Then why the change to
cancel_shielded_checkpoint()
?