From fde62c72be13c5c424d2c9017041fc92d11137cb Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 May 2024 17:04:30 -0400 Subject: [PATCH] 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. --- tractor/_supervise.py | 50 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/tractor/_supervise.py b/tractor/_supervise.py index 59ec728..8f3574b 100644 --- a/tractor/_supervise.py +++ b/tractor/_supervise.py @@ -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'