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-codedrop_ria_nursery
parent
d8af5f125a
commit
a3057cb24f
|
|
@ -1,3 +1,5 @@
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
|
|
@ -21,25 +23,36 @@ async def breakpoint_forever():
|
||||||
async def spawn_until(depth=0):
|
async def spawn_until(depth=0):
|
||||||
""""A nested nursery that triggers another ``NameError``.
|
""""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:
|
if depth < 1:
|
||||||
|
|
||||||
await n.run_in_actor(breakpoint_forever)
|
tn.start_soon(
|
||||||
|
partial(
|
||||||
p = await n.run_in_actor(
|
tractor.to_actor.run,
|
||||||
name_error,
|
breakpoint_forever,
|
||||||
name='name_error'
|
an=an,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
await trio.sleep(0.5)
|
await trio.sleep(0.5)
|
||||||
# rx and propagate error from child
|
# rx and propagate error from child
|
||||||
await p.result()
|
await tractor.to_actor.run(
|
||||||
|
name_error,
|
||||||
|
an=an,
|
||||||
|
name='name_error',
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# recusrive call to spawn another process branching layer of
|
# 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
|
depth -= 1
|
||||||
await n.run_in_actor(
|
await tractor.to_actor.run(
|
||||||
spawn_until,
|
spawn_until,
|
||||||
|
an=an,
|
||||||
depth=depth,
|
depth=depth,
|
||||||
name=f'spawn_until_{depth}',
|
name=f'spawn_until_{depth}',
|
||||||
)
|
)
|
||||||
|
|
@ -65,34 +78,33 @@ async def main():
|
||||||
└─ python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...)
|
└─ python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery(
|
async with (
|
||||||
|
tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
loglevel='pdb',
|
loglevel='pdb',
|
||||||
) as n:
|
) as an,
|
||||||
|
trio.open_nursery() as tn,
|
||||||
# spawn both actors
|
):
|
||||||
portal = await n.run_in_actor(
|
# 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,
|
spawn_until,
|
||||||
|
an=an,
|
||||||
depth=3,
|
depth=3,
|
||||||
name='spawner0',
|
name='spawner0',
|
||||||
)
|
)
|
||||||
portal1 = await n.run_in_actor(
|
)
|
||||||
|
tn.start_soon(
|
||||||
|
partial(
|
||||||
|
tractor.to_actor.run,
|
||||||
spawn_until,
|
spawn_until,
|
||||||
|
an=an,
|
||||||
depth=4,
|
depth=4,
|
||||||
name='spawner1',
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,11 @@ async def spawn_error():
|
||||||
""""A nested nursery that triggers another ``NameError``.
|
""""A nested nursery that triggers another ``NameError``.
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as n:
|
||||||
portal = await n.run_in_actor(
|
return await tractor.to_actor.run(
|
||||||
name_error,
|
name_error,
|
||||||
|
an=n,
|
||||||
name='name_error_1',
|
name='name_error_1',
|
||||||
)
|
)
|
||||||
return await portal.result()
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
@ -38,29 +38,36 @@ async def main():
|
||||||
- root actor should then fail on assert
|
- root actor should then fail on assert
|
||||||
- program termination
|
- program termination
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery(
|
async with (
|
||||||
|
tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
loglevel='devx',
|
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
|
# ..and bg-schedule their erroring tasks.
|
||||||
portal = await n.run_in_actor(
|
tn.start_soon(portal.run, name_error)
|
||||||
name_error,
|
tn.start_soon(portal1.run, spawn_error)
|
||||||
name='name_error',
|
|
||||||
)
|
# yield to the bg tasks so both RPC requests are
|
||||||
portal1 = await n.run_in_actor(
|
# submitted (and start crashing) before the root's own
|
||||||
spawn_error,
|
# error below (the legacy `run_in_actor()` submitted
|
||||||
name='spawn_error',
|
# in-line with each spawn).
|
||||||
)
|
await trio.sleep(0.5)
|
||||||
|
|
||||||
# trigger a root actor error
|
# trigger a root actor error
|
||||||
assert 0
|
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__':
|
if __name__ == '__main__':
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ async def spawn_error():
|
||||||
""""A nested nursery that triggers another ``NameError``.
|
""""A nested nursery that triggers another ``NameError``.
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as n:
|
||||||
portal = await n.run_in_actor(
|
return await tractor.to_actor.run(
|
||||||
name_error,
|
name_error,
|
||||||
|
an=n,
|
||||||
name='name_error_1',
|
name='name_error_1',
|
||||||
)
|
)
|
||||||
return await portal.result()
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
@ -36,17 +36,39 @@ async def main():
|
||||||
`-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...)
|
`-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...)
|
||||||
`-python -m tractor._child --uid ('name_error', '3391222c ...)
|
`-python -m tractor._child --uid ('name_error', '3391222c ...)
|
||||||
"""
|
"""
|
||||||
|
errors: list[BaseException] = []
|
||||||
|
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
# loglevel='runtime',
|
# loglevel='runtime',
|
||||||
) as n:
|
) as an:
|
||||||
|
|
||||||
# Spawn both actors, don't bother with collecting results
|
async def run_and_collect(fn):
|
||||||
# (would result in a different debugger outcome due to parent's
|
'''
|
||||||
# cancellation).
|
One-shot whose (boxed) error is stashed instead of
|
||||||
await n.run_in_actor(breakpoint_forever)
|
raised so a sibling's crash never cancels the others
|
||||||
await n.run_in_actor(name_error)
|
before they've had their own debugger sessions (the
|
||||||
await n.run_in_actor(spawn_error)
|
"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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
|
|
@ -10,14 +12,14 @@ async def name_error():
|
||||||
async def spawn_until(depth=0):
|
async def spawn_until(depth=0):
|
||||||
""""A nested nursery that triggers another ``NameError``.
|
""""A nested nursery that triggers another ``NameError``.
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as an:
|
||||||
if depth < 1:
|
if depth < 1:
|
||||||
# await n.run_in_actor('breakpoint_forever', breakpoint_forever)
|
await tractor.to_actor.run(name_error, an=an)
|
||||||
await n.run_in_actor(name_error)
|
|
||||||
else:
|
else:
|
||||||
depth -= 1
|
depth -= 1
|
||||||
await n.run_in_actor(
|
await tractor.to_actor.run(
|
||||||
spawn_until,
|
spawn_until,
|
||||||
|
an=an,
|
||||||
depth=depth,
|
depth=depth,
|
||||||
name=f'spawn_until_{depth}',
|
name=f'spawn_until_{depth}',
|
||||||
)
|
)
|
||||||
|
|
@ -37,28 +39,33 @@ async def main():
|
||||||
└─ python -m tractor._child --uid ('name_error', '6c2733b8 ...)
|
└─ python -m tractor._child --uid ('name_error', '6c2733b8 ...)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async with tractor.open_nursery(
|
async with (
|
||||||
|
tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
enable_transports=['uds'], # TODO, apss this via osenv?
|
enable_transports=['uds'], # TODO, apss this via osenv?
|
||||||
loglevel='devx', # XXX, required for test!
|
loglevel='devx', # XXX, required for test!
|
||||||
) as n:
|
) as an,
|
||||||
|
trio.open_nursery() as tn,
|
||||||
# spawn both actors
|
):
|
||||||
portal = await n.run_in_actor(
|
# spawn the deeper tree in the bg..
|
||||||
spawn_until,
|
tn.start_soon(
|
||||||
depth=0,
|
partial(
|
||||||
name='spawner0',
|
tractor.to_actor.run,
|
||||||
)
|
|
||||||
portal1 = await n.run_in_actor(
|
|
||||||
spawn_until,
|
spawn_until,
|
||||||
|
an=an,
|
||||||
depth=1,
|
depth=1,
|
||||||
name='spawner1',
|
name='spawner1',
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# nursery cancellation should be triggered due to propagated
|
# ..while blocking on the shallow (faster to fail) tree
|
||||||
# error from child.
|
# whose propagated error triggers nursery cancellation.
|
||||||
await portal.result()
|
await tractor.to_actor.run(
|
||||||
await portal1.result()
|
spawn_until,
|
||||||
|
an=an,
|
||||||
|
depth=0,
|
||||||
|
name='spawner0',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,24 @@ async def main():
|
||||||
simultaneously.
|
simultaneously.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async with tractor.open_nursery(
|
async with (
|
||||||
|
tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
# loglevel='debug' # ?XXX required?
|
# loglevel='debug' # ?XXX required?
|
||||||
) as n:
|
) as n,
|
||||||
|
trio.open_nursery() as tn,
|
||||||
# spawn both actors
|
):
|
||||||
portal = await n.run_in_actor(key_error)
|
# spawn the actor..
|
||||||
|
portal = await n.start_actor(
|
||||||
|
'key_error',
|
||||||
|
enable_modules=[__name__],
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
f'Child is up @ {portal.chan.aid.reprol()}'
|
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
|
# XXX: originally a bug caused by this is where root would enter
|
||||||
# the debugger and clobber the tty used by the repl even though
|
# the debugger and clobber the tty used by the repl even though
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,10 @@ async def main():
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
) as n:
|
) as n:
|
||||||
portal: tractor.Portal = await n.run_in_actor(
|
await tractor.to_actor.run(
|
||||||
cancelled_before_pause,
|
cancelled_before_pause,
|
||||||
|
an=n,
|
||||||
)
|
)
|
||||||
await portal.wait_for_result()
|
|
||||||
|
|
||||||
# ensure the same works in the root actor!
|
# ensure the same works in the root actor!
|
||||||
await pm_on_cancelled()
|
await pm_on_cancelled()
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,12 @@ async def main():
|
||||||
loglevel='cancel',
|
loglevel='cancel',
|
||||||
) as n:
|
) 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,
|
breakpoint_forever,
|
||||||
|
an=n,
|
||||||
)
|
)
|
||||||
await portal.wait_for_result()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,12 @@ async def main():
|
||||||
) as an:
|
) as an:
|
||||||
|
|
||||||
# TODO: ideally the REPL arrives at this frame in the parent,
|
# TODO: ideally the REPL arrives at this frame in the parent,
|
||||||
# ABOVE the @api_frame of `Portal.run_in_actor()` (which
|
# ABOVE the @api_frame of `to_actor.run()` ..
|
||||||
# should eventually not even be a portal method ... XD)
|
|
||||||
# await tractor.pause()
|
# await tractor.pause()
|
||||||
p: tractor.Portal = await an.run_in_actor(name_error)
|
|
||||||
|
|
||||||
# with this style, should raise on this line
|
# the one-shot blocks on the subactor's result so the
|
||||||
await p.wait_for_result()
|
# boxed `NameError` raises right here.
|
||||||
|
await tractor.to_actor.run(name_error, an=an)
|
||||||
# with this alt style should raise at `open_nusery()`
|
|
||||||
# return await p.wait_for_result()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ async def main() -> None:
|
||||||
|
|
||||||
# TODO: 3 sub-actor usage cases:
|
# TODO: 3 sub-actor usage cases:
|
||||||
# -[x] via a `.open_context()`
|
# -[x] via a `.open_context()`
|
||||||
# -[ ] via a `.run_in_actor()` call
|
# -[ ] via a `to_actor.run()` call
|
||||||
# -[ ] via a `.run()`
|
# -[ ] via a `.run()`
|
||||||
# -[ ] via a `.to_thread.run_sync()` in subactor
|
# -[ ] via a `.to_thread.run_sync()` in subactor
|
||||||
async with p.open_context(
|
async with p.open_context(
|
||||||
|
|
|
||||||
|
|
@ -849,43 +849,36 @@ def test_multi_nested_subactors_error_through_nurseries(
|
||||||
break
|
break
|
||||||
|
|
||||||
# boxed source errors
|
# 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] = [
|
expect_patts: list[str] = [
|
||||||
"NameError: name 'doggypants' is not defined",
|
|
||||||
"tractor._exceptions.RemoteActorError:",
|
"tractor._exceptions.RemoteActorError:",
|
||||||
"('name_error'",
|
|
||||||
|
|
||||||
# first level subtrees
|
# each level's unwrapped-single-member-group
|
||||||
# "tractor._exceptions.RemoteActorError: ('spawner0'",
|
# annotation + the first-level subtree's boundary
|
||||||
"src_uid=('spawner0'",
|
# footer.
|
||||||
|
"( ^^^ this exc was collapsed from a group ^^^ )",
|
||||||
# "tractor._exceptions.RemoteActorError: ('spawner1'",
|
"------ ('spawner0'",
|
||||||
|
|
||||||
# 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'",
|
|
||||||
]
|
]
|
||||||
# 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 (
|
if (
|
||||||
not is_forking_spawner
|
not is_forking_spawner
|
||||||
and
|
and
|
||||||
last_send_char == 'q'
|
last_send_char == 'q'
|
||||||
):
|
):
|
||||||
expect_patts += [
|
expect_patts += [
|
||||||
# expect the pdb-quit exc.
|
# expect the pdb-quit exc relayed from the leaf's
|
||||||
|
# bp-loop child.
|
||||||
"bdb.BdbQuit",
|
"bdb.BdbQuit",
|
||||||
# BUT WHY these dude!?
|
"src_uid=('breakpoint_forever'",
|
||||||
"src_uid=('spawn_until_0'",
|
|
||||||
"relay_uid=('spawn_until_1'",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
assert_before(
|
assert_before(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue