Show runtime nursery frames on internal errors

Much like other recent changes attempt to detect runtime-bug-causing
crashes and only show the runtime-endpoint frame when present.

Adds a `ActorNursery._scope_error: BaseException|None` attr to aid with
detection. Also toss in some todo notes for removing and replacing the
`.run_in_actor()` method API.
runtime_to_msgspec
Tyler Goodlet 2024-05-20 17:04:30 -04:00
parent 4ef77bb64f
commit fde62c72be
1 changed files with 43 additions and 7 deletions

View File

@ -84,6 +84,7 @@ class ActorNursery:
ria_nursery: trio.Nursery,
da_nursery: trio.Nursery,
errors: dict[tuple[str, str], BaseException],
) -> None:
# self.supervisor = supervisor # TODO
self._actor: Actor = actor
@ -105,6 +106,7 @@ class ActorNursery:
self._at_least_one_child_in_debug: bool = False
self.errors = errors
self.exited = trio.Event()
self._scope_error: BaseException|None = None
# NOTE: when no explicit call is made to
# `.open_root_actor()` by application code,
@ -117,7 +119,9 @@ class ActorNursery:
async def start_actor(
self,
name: str,
*,
bind_addrs: list[tuple[str, int]] = [_default_bind_addr],
rpc_module_paths: list[str]|None = None,
enable_modules: list[str]|None = None,
@ -125,6 +129,7 @@ class ActorNursery:
nursery: trio.Nursery|None = None,
debug_mode: bool|None = None,
infect_asyncio: bool = False,
) -> Portal:
'''
Start a (daemon) actor: an process that has no designated
@ -189,6 +194,13 @@ class ActorNursery:
)
)
# TODO: DEPRECATE THIS:
# -[ ] impl instead as a hilevel wrapper on
# top of a `@context` style invocation.
# |_ dynamic @context decoration on child side
# |_ implicit `Portal.open_context() as (ctx, first):`
# and `return first` on parent side.
# -[ ] use @api_frame on the wrapper
async def run_in_actor(
self,
@ -221,7 +233,7 @@ class ActorNursery:
# use the explicit function name if not provided
name = fn.__name__
portal = await self.start_actor(
portal: Portal = await self.start_actor(
name,
enable_modules=[mod_path] + (
enable_modules or rpc_module_paths or []
@ -250,6 +262,7 @@ class ActorNursery:
)
return portal
# @api_frame
async def cancel(
self,
hard_kill: bool = False,
@ -346,7 +359,12 @@ async def _open_and_supervise_one_cancels_all_nursery(
actor: Actor,
) -> typing.AsyncGenerator[ActorNursery, None]:
__tracebackhide__ = True
# normally don't need to show user by default
__tracebackhide__: bool = True
outer_err: BaseException|None = None
inner_err: BaseException|None = None
# the collection of errors retreived from spawned sub-actors
errors: dict[tuple[str, str], BaseException] = {}
@ -356,7 +374,7 @@ async def _open_and_supervise_one_cancels_all_nursery(
# handling errors that are generated by the inner nursery in
# a supervisor strategy **before** blocking indefinitely to wait for
# actors spawned in "daemon mode" (aka started using
# ``ActorNursery.start_actor()``).
# `ActorNursery.start_actor()`).
# errors from this daemon actor nursery bubble up to caller
async with trio.open_nursery() as da_nursery:
@ -391,7 +409,8 @@ async def _open_and_supervise_one_cancels_all_nursery(
)
an._join_procs.set()
except BaseException as inner_err:
except BaseException as _inner_err:
inner_err = _inner_err
errors[actor.uid] = inner_err
# If we error in the root but the debugger is
@ -469,8 +488,10 @@ async def _open_and_supervise_one_cancels_all_nursery(
Exception,
BaseExceptionGroup,
trio.Cancelled
) as _outer_err:
outer_err = _outer_err
) as err:
an._scope_error = outer_err or inner_err
# XXX: yet another guard before allowing the cancel
# sequence in case a (single) child is in debug.
@ -485,7 +506,7 @@ async def _open_and_supervise_one_cancels_all_nursery(
if an._children:
log.cancel(
'Actor-nursery cancelling due error type:\n'
f'{err}\n'
f'{outer_err}\n'
)
with trio.CancelScope(shield=True):
await an.cancel()
@ -512,6 +533,13 @@ async def _open_and_supervise_one_cancels_all_nursery(
else:
raise list(errors.values())[0]
# show frame on any (likely) internal error
if (
not an.cancelled
and an._scope_error
):
__tracebackhide__: bool = False
# da_nursery scope end - nursery checkpoint
# final exit
@ -537,7 +565,7 @@ async def open_nursery(
which cancellation scopes correspond to each spawned subactor set.
'''
__tracebackhide__ = True
__tracebackhide__: bool = True
implicit_runtime: bool = False
actor: Actor = current_actor(err_on_no_runtime=False)
an: ActorNursery|None = None
@ -588,6 +616,14 @@ async def open_nursery(
an.exited.set()
finally:
# show frame on any internal runtime-scope error
if (
an
and not an.cancelled
and an._scope_error
):
__tracebackhide__: bool = False
msg: str = (
'Actor-nursery exited\n'
f'|_{an}\n'