Port debugging examples off `run_in_actor`

The 8 `examples/debugging/` scripts driven by the pexpect'd
`test_debugger.py` REPL-flows (#477 removal),

- blocking one-shots (`subactor_error`, `subactor_breakpoint`,
  `shielded_pause`): straight `to_actor.run(fn, an=an)` — the
  boxed error/`BdbQuit` raises in the root's task.
- `multi_subactors`: introduces the "collect all errors" pattern
  — each one-shot catches + stashes its `RemoteActorError` (vs
  raising) so no child's crash cancels its siblings before
  they've had their own REPL sessions, then a
  `BaseExceptionGroup` of the lot raises at the end; preserves
  the legacy teardown-reap REPL flow exactly (28/28 debugger
  suite unchanged).
- `multi_nested_subactors_error_up_through_nurseries` +
  `root_cancelled_but_child_is_in_tty_lock`: recursive
  `spawn_until` levels each block on their one-shot child; the
  parallel spawner-trees run as bg task-nursery one-shots where
  the first tree's error cancels the other.
- `multi_subactor_root_errors` +
  `root_timeout_while_child_crashed`: `start_actor()` + bg
  `Portal.run()` tasks so the root's own error/timeout races the
  already-crashed children, same as before.
- `sync_bp`: TODO-comment x-ref update only.

`test_debugger.py`: the nested-nurseries test's final-output
patterns update to the new relay shape — the LAST-released leaf
REPL's error chain wins each level's relay-vs-cancel race and
relays as a `collapse_eg()`-annotated collapsed chain, while the
sibling tree is cancelled + absorbed. (The legacy teardown-reap
grouped BOTH the `name_error` and bp-quit chains — explaining
the previously-mysterious "extra" `src_uid`/`relay_uid` patterns
noted in the old TODO.)

Gate: `tests/devx/test_debugger.py` = 28 passed, 6 skipped —
identical to the pre-migration baseline.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
drop_ria_nursery
Gud Boi 2026-07-06 12:24:29 -04:00
parent d8af5f125a
commit a3057cb24f
10 changed files with 179 additions and 133 deletions

View File

@ -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,34 +78,33 @@ async def main():
python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...)
"""
async with tractor.open_nursery(
async with (
tractor.open_nursery(
debug_mode=True,
loglevel='pdb',
) as n:
# spawn both actors
portal = await n.run_in_actor(
) 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(
)
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__':

View File

@ -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(
async with (
tractor.open_nursery(
debug_mode=True,
loglevel='devx',
) as n:
) 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)

View File

@ -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__':

View File

@ -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(
async with (
tractor.open_nursery(
debug_mode=True,
enable_transports=['uds'], # TODO, apss this via osenv?
loglevel='devx', # XXX, required for test!
) as n:
# spawn both actors
portal = await n.run_in_actor(
spawn_until,
depth=0,
name='spawner0',
)
portal1 = await n.run_in_actor(
) 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',
)
)
# nursery cancellation should be triggered due to propagated
# error from child.
await portal.result()
await portal1.result()
# ..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',
)
if __name__ == '__main__':

View File

@ -13,17 +13,24 @@ async def main():
simultaneously.
'''
async with tractor.open_nursery(
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)
) 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

View File

@ -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()

View File

@ -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__':

View File

@ -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__':

View File

@ -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(

View File

@ -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(