Skip to content

Commit c347da3

Browse files
committed
Simplify main task outcome handling
Old way: - exceptions and regular returns from main were captured, then re-raised/returned from init, then captured again, then re-raised/returned from run() - exceptions in system tasks were converted into TrioInternalErrors (one-at-a-time, using MultiError.filter()), except for a whitelist of types (Cancelled, KeyboardInterrupt, GeneratorExit, TrioInternalError), and then re-raised in the init task. - exceptions in the run loop machinery were caught in run(), and converted into TrioInternalErrors (if they weren't already). New way: - exceptions and regular returns from main are captured, and then re-raised/returned from run() directly - exceptions in system tasks are allowed to propagate naturally into the init task - exceptions in the init task are re-raised out of the run loop machinery - exceptions in the run loop machinery are caught in run(), and converted into TrioInternalErrors (if they aren't already). This needs one new special case to detect when spawning the main task itself errors, and treating that as a regular non-TrioInternalError, but otherwise it simplifies things a lot. And, it removes 2 unnecessary traceback frames from every trio traceback. Removing the special case handling for some exception types in system tasks did break a few tests. It's not as bad as it seems though: - Cancelled was allowed through so it could reach the system nursery's __aexit__; that still happens. But now if it's not caught there, it gets converted into TrioInternalError instead of being allowed to escape from trio.run(). - KeyboardInterrupt should never happen in system tasks anyway; not sure why we had a special case to allow this. - GeneratorExit should never happen; if it does, it's probably because things blew up real good, and then the system task coroutine got GC'ed, and called coro.close(). In this case letting it escape is the right thing to do; coro.close() will catch it. In other cases, letting it escape and get converted into a TrioInternalError is fine. - Letting TrioInternalError through works the same as before. Also, if multiple system tasks crash, we now get a single TrioInternalError with the original MultiError as a __cause__, rather than a MultiError containing multiple TrioInternalErrors. This is probably less confusing, and it's more compatible with the python-triogh-611 approach to things.
1 parent 7265ca2 commit c347da3

File tree

2 files changed

+32
-54
lines changed

2 files changed

+32
-54
lines changed

trio/_core/_run.py

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,6 @@ class Runner:
667667
deadlines = attr.ib(default=attr.Factory(SortedDict))
668668

669669
init_task = attr.ib(default=None)
670-
init_task_result = attr.ib(default=None)
671670
system_nursery = attr.ib(default=None)
672671
system_context = attr.ib(default=None)
673672
main_task = attr.ib(default=None)
@@ -931,22 +930,21 @@ def task_exited(self, task, result):
931930
while task._cancel_stack:
932931
task._cancel_stack[-1]._remove_task(task)
933932
self.tasks.remove(task)
934-
if task._parent_nursery is None:
933+
if task is self.main_task:
934+
self.main_task_result = result
935+
self.system_nursery.cancel_scope.cancel()
936+
self.system_nursery._child_finished(task, Value(None))
937+
elif task is self.init_task:
938+
# If the init task crashed, then something is very wrong and we
939+
# let the error propagate. (It'll eventually be wrapped in a
940+
# TrioInternalError.)
941+
result.unwrap()
935942
# the init task should be the last task to exit. If not, then
936-
# something is very wrong. Probably it hit some unexpected error,
937-
# in which case we re-raise the error (which will later get
938-
# converted to a TrioInternalError, but at least we'll get a
939-
# traceback). Otherwise, raise a new error.
943+
# something is very wrong.
940944
if self.tasks: # pragma: no cover
941-
result.unwrap()
942945
raise TrioInternalError
943946
else:
944947
task._parent_nursery._child_finished(task, result)
945-
if task is self.main_task:
946-
self.main_task_result = result
947-
self.system_nursery.cancel_scope.cancel()
948-
if task is self.init_task:
949-
self.init_task_result = result
950948

951949
if self.instruments:
952950
self.instrument("task_exited", task)
@@ -973,7 +971,10 @@ def spawn_system_task(self, async_fn, *args, name=None):
973971
974972
* By default, system tasks have :exc:`KeyboardInterrupt` protection
975973
*enabled*. If you want your task to be interruptible by control-C,
976-
then you need to use :func:`disable_ki_protection` explicitly.
974+
then you need to use :func:`disable_ki_protection` explicitly (and
975+
come up with some plan for what to do with a
976+
:exc:`KeyboardInterrupt`, given that system tasks aren't allowed to
977+
raise exceptions).
977978
978979
* System tasks do not inherit context variables from their creator.
979980
@@ -993,40 +994,21 @@ def spawn_system_task(self, async_fn, *args, name=None):
993994
994995
"""
995996

996-
async def system_task_wrapper(async_fn, args):
997-
PASS = (
998-
Cancelled, KeyboardInterrupt, GeneratorExit, TrioInternalError
999-
)
1000-
1001-
def excfilter(exc):
1002-
if isinstance(exc, PASS):
1003-
return exc
1004-
else:
1005-
new_exc = TrioInternalError("system task crashed")
1006-
new_exc.__cause__ = exc
1007-
return new_exc
1008-
1009-
with MultiError.catch(excfilter):
1010-
await async_fn(*args)
1011-
1012-
if name is None:
1013-
name = async_fn
1014997
return self.spawn_impl(
1015-
system_task_wrapper,
1016-
(async_fn, args),
1017-
self.system_nursery,
1018-
name,
1019-
system_task=True,
998+
async_fn, args, self.system_nursery, name, system_task=True
1020999
)
10211000

10221001
async def init(self, async_fn, args):
10231002
async with open_nursery() as system_nursery:
10241003
self.system_nursery = system_nursery
1025-
self.main_task = self.spawn_impl(
1026-
async_fn, args, system_nursery, None
1027-
)
1004+
try:
1005+
self.main_task = self.spawn_impl(
1006+
async_fn, args, system_nursery, None
1007+
)
1008+
except BaseException as exc:
1009+
self.main_task_result = Error(exc)
1010+
system_nursery.cancel_scope.cancel()
10281011
self.entry_queue.spawn()
1029-
return self.main_task_result.unwrap()
10301012

10311013
################
10321014
# Outside context problems
@@ -1326,7 +1308,7 @@ def run(
13261308
with closing(runner):
13271309
# The main reason this is split off into its own function
13281310
# is just to get rid of this extra indentation.
1329-
result = run_impl(runner, async_fn, args)
1311+
run_impl(runner, async_fn, args)
13301312
except TrioInternalError:
13311313
raise
13321314
except BaseException as exc:
@@ -1335,7 +1317,7 @@ def run(
13351317
) from exc
13361318
finally:
13371319
GLOBAL_RUN_CONTEXT.__dict__.clear()
1338-
return result.unwrap()
1320+
return runner.main_task_result.unwrap()
13391321
finally:
13401322
# To guarantee that we never swallow a KeyboardInterrupt, we have to
13411323
# check for pending ones once more after leaving the context manager:
@@ -1504,8 +1486,6 @@ def run_impl(runner, async_fn, args):
15041486
runner.instrument("after_task_step", task)
15051487
del GLOBAL_RUN_CONTEXT.task
15061488

1507-
return runner.init_task_result
1508-
15091489

15101490
################################################################
15111491
# Other public API functions

trio/_core/tests/test_run.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -960,15 +960,14 @@ async def main():
960960
_core.spawn_system_task(system_task)
961961
await sleep_forever()
962962

963-
with pytest.raises(_core.MultiError) as excinfo:
963+
with pytest.raises(_core.TrioInternalError) as excinfo:
964964
_core.run(main)
965965

966-
assert len(excinfo.value.exceptions) == 2
967-
cause_types = set()
968-
for exc in excinfo.value.exceptions:
969-
assert type(exc) is _core.TrioInternalError
970-
cause_types.add(type(exc.__cause__))
971-
assert cause_types == {KeyError, ValueError}
966+
me = excinfo.value.__cause__
967+
assert isinstance(me, _core.MultiError)
968+
assert len(me.exceptions) == 2
969+
for exc in me.exceptions:
970+
assert isinstance(exc, (KeyError, ValueError))
972971

973972

974973
def test_system_task_crash_plus_Cancelled():
@@ -1005,10 +1004,9 @@ async def main():
10051004
_core.spawn_system_task(ki)
10061005
await sleep_forever()
10071006

1008-
# KI doesn't get wrapped with TrioInternalError
1009-
with pytest.raises(KeyboardInterrupt):
1007+
with pytest.raises(_core.TrioInternalError) as excinfo:
10101008
_core.run(main)
1011-
1009+
assert isinstance(excinfo.value.__cause__, KeyboardInterrupt)
10121010

10131011
# This used to fail because checkpoint was a yield followed by an immediate
10141012
# reschedule. So we had:

0 commit comments

Comments
 (0)