diff --git a/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py b/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py index 6cfce50f..fe50d9a4 100644 --- a/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py +++ b/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py @@ -1,3 +1,5 @@ +from functools import partial + import trio import tractor @@ -21,25 +23,36 @@ async def breakpoint_forever(): async def spawn_until(depth=0): """"A nested nursery that triggers another ``NameError``. """ - async with tractor.open_nursery() as n: + async with ( + tractor.open_nursery() as an, + trio.open_nursery() as tn, + ): if depth < 1: - await n.run_in_actor(breakpoint_forever) - - p = await n.run_in_actor( - name_error, - name='name_error' + tn.start_soon( + partial( + tractor.to_actor.run, + breakpoint_forever, + an=an, + ) ) + await trio.sleep(0.5) # rx and propagate error from child - await p.result() + await tractor.to_actor.run( + name_error, + an=an, + name='name_error', + ) else: # recusrive call to spawn another process branching layer of - # the tree + # the tree; blocks (up) each level until the leaf's + # `name_error` relays through. depth -= 1 - await n.run_in_actor( + await tractor.to_actor.run( spawn_until, + an=an, depth=depth, name=f'spawn_until_{depth}', ) @@ -65,35 +78,34 @@ async def main(): └─ python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...) """ - async with tractor.open_nursery( - debug_mode=True, - loglevel='pdb', - ) as n: - - # spawn both actors - portal = await n.run_in_actor( - spawn_until, - depth=3, - name='spawner0', + async with ( + tractor.open_nursery( + debug_mode=True, + loglevel='pdb', + ) as an, + trio.open_nursery() as tn, + ): + # spawn both spawner trees as concurrent one-shots; the + # first tree's (relayed) error cancels the other. + tn.start_soon( + partial( + tractor.to_actor.run, + spawn_until, + an=an, + depth=3, + name='spawner0', + ) ) - portal1 = await n.run_in_actor( - spawn_until, - depth=4, - name='spawner1', + tn.start_soon( + partial( + tractor.to_actor.run, + spawn_until, + an=an, + depth=4, + name='spawner1', + ) ) - # TODO: test this case as well where the parent don't see - # the sub-actor errors by default and instead expect a user - # ctrl-c to kill the root. - with trio.move_on_after(3): - await trio.sleep_forever() - - # gah still an issue here. - await portal.result() - - # should never get here - await portal1.result() - if __name__ == '__main__': trio.run(main) diff --git a/examples/debugging/multi_subactor_root_errors.py b/examples/debugging/multi_subactor_root_errors.py index 31bb7dd1..56f217b4 100644 --- a/examples/debugging/multi_subactor_root_errors.py +++ b/examples/debugging/multi_subactor_root_errors.py @@ -16,11 +16,11 @@ async def spawn_error(): """"A nested nursery that triggers another ``NameError``. """ async with tractor.open_nursery() as n: - portal = await n.run_in_actor( + return await tractor.to_actor.run( name_error, + an=n, name='name_error_1', - ) - return await portal.result() + ) async def main(): @@ -38,29 +38,36 @@ async def main(): - root actor should then fail on assert - program termination """ - async with tractor.open_nursery( - debug_mode=True, - loglevel='devx', - ) as n: + async with ( + tractor.open_nursery( + debug_mode=True, + loglevel='devx', + ) as an, + trio.open_nursery() as tn, + ): + # spawn both actors.. + portal = await an.start_actor( + 'name_error', + enable_modules=[__name__], + ) + portal1 = await an.start_actor( + 'spawn_error', + enable_modules=[__name__], + ) - # spawn both actors - portal = await n.run_in_actor( - name_error, - name='name_error', - ) - portal1 = await n.run_in_actor( - spawn_error, - name='spawn_error', - ) + # ..and bg-schedule their erroring tasks. + tn.start_soon(portal.run, name_error) + tn.start_soon(portal1.run, spawn_error) + + # yield to the bg tasks so both RPC requests are + # submitted (and start crashing) before the root's own + # error below (the legacy `run_in_actor()` submitted + # in-line with each spawn). + await trio.sleep(0.5) # trigger a root actor error assert 0 - # attempt to collect results (which raises error in parent) - # still has some issues where the parent seems to get stuck - await portal.result() - await portal1.result() - if __name__ == '__main__': trio.run(main) diff --git a/examples/debugging/multi_subactors.py b/examples/debugging/multi_subactors.py index 57634cc3..79775053 100644 --- a/examples/debugging/multi_subactors.py +++ b/examples/debugging/multi_subactors.py @@ -18,11 +18,11 @@ async def spawn_error(): """"A nested nursery that triggers another ``NameError``. """ async with tractor.open_nursery() as n: - portal = await n.run_in_actor( + return await tractor.to_actor.run( name_error, + an=n, name='name_error_1', ) - return await portal.result() async def main(): @@ -36,17 +36,39 @@ async def main(): `-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...) `-python -m tractor._child --uid ('name_error', '3391222c ...) """ + errors: list[BaseException] = [] + async with tractor.open_nursery( debug_mode=True, # loglevel='runtime', - ) as n: + ) as an: - # Spawn both actors, don't bother with collecting results - # (would result in a different debugger outcome due to parent's - # cancellation). - await n.run_in_actor(breakpoint_forever) - await n.run_in_actor(name_error) - await n.run_in_actor(spawn_error) + async def run_and_collect(fn): + ''' + One-shot whose (boxed) error is stashed instead of + raised so a sibling's crash never cancels the others + before they've had their own debugger sessions (the + "collect all errors" the legacy `run_in_actor()` API + did implicitly at nursery teardown). + + ''' + try: + await tractor.to_actor.run(fn, an=an) + except tractor.RemoteActorError as rae: + errors.append(rae) + + # Spawn all one-shot task actors, collecting (vs. + # raising) their errors. + async with trio.open_nursery() as tn: + tn.start_soon(run_and_collect, breakpoint_forever) + tn.start_soon(run_and_collect, name_error) + tn.start_soon(run_and_collect, spawn_error) + + if errors: + raise BaseExceptionGroup( + 'multi_subactors errored!', + errors, + ) if __name__ == '__main__': diff --git a/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py b/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py index 93daa33b..083a7fb3 100644 --- a/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py +++ b/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py @@ -1,3 +1,5 @@ +from functools import partial + import trio import tractor @@ -10,14 +12,14 @@ async def name_error(): async def spawn_until(depth=0): """"A nested nursery that triggers another ``NameError``. """ - async with tractor.open_nursery() as n: + async with tractor.open_nursery() as an: if depth < 1: - # await n.run_in_actor('breakpoint_forever', breakpoint_forever) - await n.run_in_actor(name_error) + await tractor.to_actor.run(name_error, an=an) else: depth -= 1 - await n.run_in_actor( + await tractor.to_actor.run( spawn_until, + an=an, depth=depth, name=f'spawn_until_{depth}', ) @@ -37,28 +39,33 @@ async def main(): └─ python -m tractor._child --uid ('name_error', '6c2733b8 ...) ''' - async with tractor.open_nursery( - debug_mode=True, - enable_transports=['uds'], # TODO, apss this via osenv? - loglevel='devx', # XXX, required for test! - ) as n: + async with ( + tractor.open_nursery( + debug_mode=True, + enable_transports=['uds'], # TODO, apss this via osenv? + loglevel='devx', # XXX, required for test! + ) as an, + trio.open_nursery() as tn, + ): + # spawn the deeper tree in the bg.. + tn.start_soon( + partial( + tractor.to_actor.run, + spawn_until, + an=an, + depth=1, + name='spawner1', + ) + ) - # spawn both actors - portal = await n.run_in_actor( + # ..while blocking on the shallow (faster to fail) tree + # whose propagated error triggers nursery cancellation. + await tractor.to_actor.run( spawn_until, + an=an, depth=0, name='spawner0', ) - portal1 = await n.run_in_actor( - spawn_until, - depth=1, - name='spawner1', - ) - - # nursery cancellation should be triggered due to propagated - # error from child. - await portal.result() - await portal1.result() if __name__ == '__main__': diff --git a/examples/debugging/root_timeout_while_child_crashed.py b/examples/debugging/root_timeout_while_child_crashed.py index 4dfc699d..28cb2cf8 100644 --- a/examples/debugging/root_timeout_while_child_crashed.py +++ b/examples/debugging/root_timeout_while_child_crashed.py @@ -13,17 +13,24 @@ async def main(): simultaneously. ''' - async with tractor.open_nursery( - debug_mode=True, - # loglevel='debug' # ?XXX required? - ) as n: - - # spawn both actors - portal = await n.run_in_actor(key_error) + async with ( + tractor.open_nursery( + debug_mode=True, + # loglevel='debug' # ?XXX required? + ) as n, + trio.open_nursery() as tn, + ): + # spawn the actor.. + portal = await n.start_actor( + 'key_error', + enable_modules=[__name__], + ) print( f'Child is up @ {portal.chan.aid.reprol()}' ) - + # ..then schedule its erroring task in the bg while the + # root blocks below. + tn.start_soon(portal.run, key_error) # XXX: originally a bug caused by this is where root would enter # the debugger and clobber the tty used by the repl even though diff --git a/examples/debugging/shielded_pause.py b/examples/debugging/shielded_pause.py index e6df907c..190374ca 100644 --- a/examples/debugging/shielded_pause.py +++ b/examples/debugging/shielded_pause.py @@ -75,10 +75,10 @@ async def main(): async with tractor.open_nursery( debug_mode=True, ) as n: - portal: tractor.Portal = await n.run_in_actor( + await tractor.to_actor.run( cancelled_before_pause, + an=n, ) - await portal.wait_for_result() # ensure the same works in the root actor! await pm_on_cancelled() diff --git a/examples/debugging/subactor_breakpoint.py b/examples/debugging/subactor_breakpoint.py index 67a5b7e0..33b16154 100644 --- a/examples/debugging/subactor_breakpoint.py +++ b/examples/debugging/subactor_breakpoint.py @@ -19,10 +19,12 @@ async def main(): loglevel='cancel', ) as n: - portal = await n.run_in_actor( + # parks awaiting a result which only arrives once the + # user quits (`BdbQuit`s) the child's REPL loop. + await tractor.to_actor.run( breakpoint_forever, + an=n, ) - await portal.wait_for_result() if __name__ == '__main__': diff --git a/examples/debugging/subactor_error.py b/examples/debugging/subactor_error.py index fabdcedb..95c1fe12 100644 --- a/examples/debugging/subactor_error.py +++ b/examples/debugging/subactor_error.py @@ -12,16 +12,12 @@ async def main(): ) as an: # TODO: ideally the REPL arrives at this frame in the parent, - # ABOVE the @api_frame of `Portal.run_in_actor()` (which - # should eventually not even be a portal method ... XD) + # ABOVE the @api_frame of `to_actor.run()` .. # await tractor.pause() - p: tractor.Portal = await an.run_in_actor(name_error) - # with this style, should raise on this line - await p.wait_for_result() - - # with this alt style should raise at `open_nusery()` - # return await p.wait_for_result() + # the one-shot blocks on the subactor's result so the + # boxed `NameError` raises right here. + await tractor.to_actor.run(name_error, an=an) if __name__ == '__main__': diff --git a/examples/debugging/sync_bp.py b/examples/debugging/sync_bp.py index 8c4ba6e9..c22f0fff 100644 --- a/examples/debugging/sync_bp.py +++ b/examples/debugging/sync_bp.py @@ -90,7 +90,7 @@ async def main() -> None: # TODO: 3 sub-actor usage cases: # -[x] via a `.open_context()` - # -[ ] via a `.run_in_actor()` call + # -[ ] via a `to_actor.run()` call # -[ ] via a `.run()` # -[ ] via a `.to_thread.run_sync()` in subactor async with p.open_context( diff --git a/tests/devx/test_debugger.py b/tests/devx/test_debugger.py index dfcf36d8..e2b803aa 100644 --- a/tests/devx/test_debugger.py +++ b/tests/devx/test_debugger.py @@ -849,43 +849,36 @@ def test_multi_nested_subactors_error_through_nurseries( break # boxed source errors + # + # NB post-#477 (`to_actor.run()` one-shots in local + # task-nurseries) the final relay is the LAST-released + # (leaf) REPL's error chain: it wins each level's + # relay-vs-cancel race so every level's single-member + # group gets unwrapped by the runtime's `collapse_eg()` + # (annotated at each actor boundary) while the sibling + # tree ('spawner1') is cancelled + absorbed. The legacy + # `run_in_actor()` teardown-reap instead grouped BOTH the + # `name_error` and bp-quit chains into the final dump + # (the previously-unexplained "extra" patterns). expect_patts: list[str] = [ - "NameError: name 'doggypants' is not defined", "tractor._exceptions.RemoteActorError:", - "('name_error'", - # first level subtrees - # "tractor._exceptions.RemoteActorError: ('spawner0'", - "src_uid=('spawner0'", - - # "tractor._exceptions.RemoteActorError: ('spawner1'", - - # propagation of errors up through nested subtrees - # "tractor._exceptions.RemoteActorError: ('spawn_until_0'", - # "tractor._exceptions.RemoteActorError: ('spawn_until_1'", - # "tractor._exceptions.RemoteActorError: ('spawn_until_2'", - # ^-NOTE-^ old RAE repr, new one is below with a field - # showing the src actor's uid. - "src_uid=('spawn_until_2'", + # each level's unwrapped-single-member-group + # annotation + the first-level subtree's boundary + # footer. + "( ^^^ this exc was collapsed from a group ^^^ )", + "------ ('spawner0'", ] - # XXX, I HAVE NO IDEA why these patts only show on the - # `trio`-spawner but it seems to have something to do with - # what gets dumped in prior-prompt latches somehow?? - # TODO for claude, explain and or work through how this is - # happening but ONLY WHEN RUN FROM THE TEST, bc when i try to - # run the test script manually the correct output ALWAYS seems - # to be in the last `str(child.before.decode())` output !?!? if ( not is_forking_spawner and last_send_char == 'q' ): expect_patts += [ - # expect the pdb-quit exc. + # expect the pdb-quit exc relayed from the leaf's + # bp-loop child. "bdb.BdbQuit", - # BUT WHY these dude!? - "src_uid=('spawn_until_0'", - "relay_uid=('spawn_until_1'", + "src_uid=('breakpoint_forever'", ] assert_before(