diff --git a/examples/debugging/multi_daemon_subactors.py b/examples/debugging/multi_daemon_subactors.py new file mode 100644 index 0000000..b37f47a --- /dev/null +++ b/examples/debugging/multi_daemon_subactors.py @@ -0,0 +1,31 @@ +import tractor +import trio + + +async def breakpoint_forever(): + "Indefinitely re-enter debugger in child actor." + while True: + yield 'yo' + await tractor.breakpoint() + + +async def name_error(): + "Raise a ``NameError``" + getattr(doggypants) + + +async def main(): + """Test breakpoint in a streaming actor. + """ + async with tractor.open_nursery() as n: + + p0 = await n.start_actor('bp_forever', rpc_module_paths=[__name__]) + p1 = await n.start_actor('name_error', rpc_module_paths=[__name__]) + + # retreive results + stream = await p0.run(__name__, 'breakpoint_forever') + await p1.run(__name__, 'name_error') + + +if __name__ == '__main__': + tractor.run(main, debug_mode=True, loglevel='error') diff --git a/tests/test_debugger.py b/tests/test_debugger.py index 5ea4f71..fc94b6f 100644 --- a/tests/test_debugger.py +++ b/tests/test_debugger.py @@ -282,6 +282,34 @@ def test_multi_subactors(spawn): assert 'bdb.BdbQuit' in before +def test_multi_daemon_subactors(spawn): + """Multiple daemon subactors, both erroring and breakpointing within a + stream. + """ + child = spawn('multi_daemon_subactors') + + child.expect(r"\(Pdb\+\+\)") + + before = str(child.before.decode()) + assert "Attaching pdb to actor: ('bp_forever'" in before + + child.sendline('c') + + # first name_error failure + child.expect(r"\(Pdb\+\+\)") + before = str(child.before.decode()) + assert "NameError" in before + + child.sendline('c') + + child.expect(r"\(Pdb\+\+\)") + before = str(child.before.decode()) + assert "tractor._exceptions.RemoteActorError: ('name_error'" in before + + child.sendline('c') + child.expect(pexpect.EOF) + + def test_multi_subactors_root_errors(spawn): """Multiple subactors, both erroring and breakpointing as well as a nested subactor erroring. diff --git a/tractor/__init__.py b/tractor/__init__.py index 5ed843f..adaad38 100644 --- a/tractor/__init__.py +++ b/tractor/__init__.py @@ -20,8 +20,8 @@ from ._state import current_actor from . import _state from ._exceptions import RemoteActorError, ModuleNotExposed from ._debug import breakpoint, post_mortem -from . import msg from . import _spawn +from . import msg __all__ = [ @@ -60,16 +60,23 @@ async def _main( """ logger = log.get_logger('tractor') + # mark top most level process as root actor + _state._runtime_vars['_is_root'] = True + if start_method is not None: _spawn.try_set_start_method(start_method) if debug_mode and _spawn._spawn_method == 'trio': _state._runtime_vars['_debug_mode'] = True + # expose internal debug module to every actor allowing # for use of ``await tractor.breakpoint()`` kwargs.setdefault('rpc_module_paths', []).append('tractor._debug') + elif debug_mode: - raise RuntimeError("Debug mode is only supported for the `trio` backend!") + raise RuntimeError( + "Debug mode is only supported for the `trio` backend!" + ) main = partial(async_fn, *args) @@ -134,9 +141,6 @@ def run( This is tractor's main entry and the start point for any async actor. """ - # mark top most level process as root actor - _state._runtime_vars['_is_root'] = True - return trio.run( partial( # our entry diff --git a/tractor/_actor.py b/tractor/_actor.py index 178e12a..6d223b0 100644 --- a/tractor/_actor.py +++ b/tractor/_actor.py @@ -301,7 +301,18 @@ class Actor: try: return getattr(self._mods[ns], funcname) except KeyError as err: - raise ModuleNotExposed(*err.args) + mne = ModuleNotExposed(*err.args) + + if ns == '__main__': + msg = ( + "\n\nMake sure you exposed the current module using:\n\n" + "ActorNursery.start_actor(, rpc_module_paths=" + "[__name__])" + ) + + mne.msg += msg + + raise mne async def _stream_handler( self, @@ -591,7 +602,7 @@ class Actor: # Receive runtime state from our parent parent_data = await chan.recv() log.debug( - "Recieved state from parent:\n" + "Received state from parent:\n" f"{parent_data}" ) accept_addr = ( @@ -599,6 +610,7 @@ class Actor: parent_data.pop('bind_port'), ) rvs = parent_data.pop('_runtime_vars') + log.debug(f"Runtime vars are: {rvs}") rvs['_is_root'] = False _state._runtime_vars.update(rvs) diff --git a/tractor/_discovery.py b/tractor/_discovery.py index 13da85d..360b16d 100644 --- a/tractor/_discovery.py +++ b/tractor/_discovery.py @@ -42,6 +42,7 @@ async def get_root( **kwargs, ) -> typing.AsyncGenerator[Union[Portal, LocalPortal], None]: host, port = _runtime_vars['_root_mailbox'] + assert host is not None async with _connect_chan(host, port) as chan: async with open_portal(chan, **kwargs) as portal: yield portal