-
-
Notifications
You must be signed in to change notification settings - Fork 378
Description
I noticed that nurseries might inject Cancelled exception even when none of the tasks have received a Cancelled. I have two different examples reproducing this issue.
A nursery with a single task raising an exception
The following code runs a nursery with a single task raising an exception while the outer scope has been cancelled:
async def main():
with trio.CancelScope() as scope:
try:
async with trio.open_nursery() as nursery:
scope.cancel()
raise RuntimeError
except trio.MultiError as exc:
print("Exceptions:", exc)
print("Cancelled caught:", nursery.cancel_scope.cancelled_caught)It prints:
Exceptions: RuntimeError(), Cancelled()
Cancelled caught: False
The Cancelled() exception is injected here:
Lines 845 to 851 in fbe3bd3
| # Nothing to wait for, so just execute a checkpoint -- but we | |
| # still need to mix any exception (e.g. from an external | |
| # cancellation) in with the rest of our exceptions. | |
| try: | |
| await checkpoint() | |
| except BaseException as exc: | |
| self._add_exc(exc) |
A potential fix could to be to use cancel_shielded_checkpoint() instead of checkpoint().
A nursery with a two tasks, both raising an exception
The following code runs a nursery with two tasks both raising an exception while the outer scope has been cancelled:
async def main():
with trio.CancelScope() as scope:
try:
async with trio.open_nursery() as nursery:
nursery.start_soon(_raise, RuntimeError(1))
scope.cancel()
raise RuntimeError(2)
except trio.MultiError as exc:
print("Exceptions:", exc)
print("Cancelled caught:", nursery.cancel_scope.cancelled_caught)It prints:
Exceptions: RuntimeError(2), Cancelled(), RuntimeError(1)
Cancelled caught: False
The Cancelled() exception is injected here:
Lines 835 to 840 in fbe3bd3
| # If we get cancelled (or have an exception injected, like | |
| # KeyboardInterrupt), then save that, but still wait until our | |
| # children finish. | |
| def aborted(raise_cancel): | |
| self._add_exc(capture(raise_cancel).error) | |
| return Abort.FAILED |
I don't see the point of injecting a Cancelled here, since the cancellation is going to be ignored anyway: isn't it a rule of trio that a cancelled (in the sense of cancel_called) but completed operation should not raise a Cancelled?
Raising Cancelled means that the operation did not happen.
https://trio.readthedocs.io/en/latest/reference-core.html#cancellable-primitives
I feel like it would be more consistent with the trio cancellation semantics to see the nursery cleanup as:
try:
yield nursery
finally:
with trio.CancelScope(shield=True):
await nursery.join_tasks()