8000 Nursery: don't act as a cancellation point in __aexit__ · python-trio/trio@a3d9d25 · GitHub
[go: up one dir, main page]

Skip to content

Commit a3d9d25

Browse files
committed
Nursery: don't act as a cancellation point in __aexit__
Previously, a Nursery could raise Cancelled even if all its children had completed successfully. This is undesirable, as discussed in #1457, so now a Nursery will shield itself from cancels in __aexit__. A Nursery with children can still be cancelled fine: if any of its children raise Cancelled, then the whole Nursery will raise Cancelled. If none of the Nursery's children raise Cancelled, then the Nursery will never raise Cancelled. As another consequence, a Nursery which never had any child tasks will never raise Cancelled. As yet another consequence, since an internal Nursery is used in the implementation of Nursery.start(), if start() is cancelled, but the task it's starting does not raise Cancelled, start() won't raise Cancelled either.
1 parent 5c7e5ef commit a3d9d25

File tree

2 files changed

+29
-22
lines changed

2 files changed

+29
-22
lines changed

trio/_core/_run.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -916,21 +916,25 @@ async def _nested_child_finished(self, nested_child_exc):
916916
self._check_nursery_closed()
917917

918918
if not self._closed:
919-
# If we get cancelled (or have an exception injected, like
920-
# KeyboardInterrupt), then save that, but still wait until our
921-
# children finish.
919+
# If we have an exception injected, like KeyboardInterrupt,
920+
# then save that, but still wait until our children finish.
922921
def aborted(raise_cancel):
923-
self._add_exc(capture(raise_cancel).error)
922+
exn = capture(raise_cancel).error
923+
# We ignore Cancelled, since we should get it again
924+
# through our children, if they were actually cancelled
925+
# rather than successfully completing.
926+
if not isinstance(exn, Cancelled):
927+
self._add_exc(exn)
924928
return Abort.FAILED
925929

926930
self._parent_waiting_in_aexit = True
927931
await wait_task_rescheduled(aborted)
928932
else:
929-
# Nothing to wait for, so just execute a checkpoint -- but we
930-
# still need to mix any exception (e.g. from an external
931-
# cancellation) in with the rest of our exceptions.
933+
# Nothing to wait for, so execute a schedule point, but don't
934+
# allow us to be cancelled, just like the other branch. We
935+
# still need to catch and store non-Cancelled exceptions.
932936
try:
933-
await checkpoint()
937+
await cancel_shielded_checkpoint()
934938
except BaseException as exc:
935939
self._add_exc(exc)
936940

trio/_core/tests/test_run.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
Sequencer,
3333
assert_checkpoints,
3434
)
35+
from ...testing._checkpoints import assert_yields_or_not
3536

3637

3738
# slightly different from _timeouts.sleep_forever because it returns the value
@@ -433,12 +434,12 @@ async def crasher():
433434
# nursery block exited, all cancellations inside the
434435
# nursery block continue propagating to reach the
435436
# outer scope.
436-
assert len(multi_exc.exceptions) == 5
437+
assert len(multi_exc.exceptions) == 4
437438
summary = {}
438439
for exc in multi_exc.exceptions:
439440
summary.setdefault(type(exc), 0)
440441
summary[type(exc)] += 1
441-
assert summary == {_core.Cancelled: 4, KeyError: 1}
442+
assert summary == {_core.Cancelled: 3, KeyError: 1}
442443
raise
443444
except AssertionError: # pragma: no cover
444445
raise
@@ -1605,20 +1606,15 @@ async def test_trivial_yields():
16051606
await _core.checkpoint_if_cancelled()
16061607
await _core.cancel_shielded_checkpoint()
16071608

1608-
with assert_checkpoints():
1609+
with assert_yields_or_not(expect_cancel_point=False, expect_schedule_point=True):
16091610
async with _core.open_nursery():
16101611
pass
16111612

16121613
with _core.CancelScope() as cancel_scope:
16131614
cancel_scope.cancel()
1614-
with pytest.raises(_core.MultiError) as excinfo:
1615+
with pytest.raises(KeyError):
16151616
async with _core.open_nursery():
16161617
raise KeyError
1617-
assert len(excinfo.value.exceptions) == 2
1618-
assert {type(e) for e in excinfo.value.exceptions} == {
1619-
KeyError,
1620-
_core.Cancelled,
1621-
}
16221618

16231619

16241620
async def test_nursery_start(autojump_clock):
@@ -1685,13 +1681,24 @@ async def nothing(task_status=_core.TASK_STATUS_IGNORED):
16851681
# is ignored; start() raises Cancelled.
16861682
async def just_started(task_status=_core.TASK_STATUS_IGNORED):
16871683
task_status.started("hi")
1684+
await _core.checkpoint()
16881685

16891686
async with _core.open_nursery() as nursery:
16901687
with _core.CancelScope() as cs:
16911688
cs.cancel()
16921689
with pytest.raises(_core.Cancelled):
16931690
await nursery.start(just_started)
16941691

1692+
# but if the task does not execute any checkpoints, and exits, then start()
1693+
# doesn't raise Cancelled, since the task completed successfully.
1694+
async def started_with_no_checkpoint(task_status=_core.TASK_STATUS_IGNORED):
1695+
task_status.started("hi")
1696+
1697+
async with _core.open_nursery() as nursery:
1698+
with _core.CancelScope() as cs:
1699+
cs.cancel()
1700+
await nursery.start(started_with_no_checkpoint)
1701+
16951702
# and if after the no-op started(), the child crashes, the error comes out
16961703
# of start()
16971704
async def raise_keyerror_after_started(task_status=_core.TASK_STATUS_IGNORED,):
@@ -1701,12 +1708,8 @@ async def raise_keyerror_after_started(task_status=_core.TASK_STATUS_IGNORED,):
17011708
async with _core.open_nursery() as nursery:
17021709
with _core.CancelScope() as cs:
17031710
cs.cancel()
1704-
with pytest.raises(_core.MultiError) as excinfo:
1711+
with pytest.raises(KeyError):
17051712
await nursery.start(raise_keyerror_after_started)
1706-
assert {type(e) for e in excinfo.value.exceptions} == {
1707-
_core.Cancelled,
1708-
KeyError,
1709-
}
17101713

17111714
# trying to start in a closed nursery raises an error immediately
17121715
async with _core.open_nursery() as closed_nursery:

0 commit comments

Comments
 (0)
0