Compare commits

...

6 Commits

Author SHA1 Message Date
Tyler Goodlet 9a70a214c1 Bah! just refine `devx.pformat.nest_from_op()`now!
Since we're gonna prolly start using it for serious..
- drop `back_from_op`.
- rename `tree_str` -> `text`
- move the huge comment-doc for "sclang" into the fn body.
- change all usage to reflect.
2025-06-17 17:47:27 -04:00
Tyler Goodlet 2bd8bf16d7 Re-impl `.devx.nest_from_op()` yet again XD
Apparently having lots of trouble getting the nested indenting
correct.. shh

This hopefully resolves it by doing the indent calcs incrementally
in the order of,
- `input_op: str` the "sclang" operator chars,
- `nest_prefix: str` the hierarchy chars, by def our
  little sub-tree L: '|_',
- finally the `tree_str` so there's no-overlap-/adjacency-to the
  `nest_prefix`.

Also deprecate (kinda) the `back_from_op` param, `nest_indent` is
more or less the replacement.
2025-06-17 17:02:21 -04:00
Tyler Goodlet 1d8230716c Flip a couple more debug scripts to UDS tpt
For now just as sanity that we're not breaking anything on that
transport backend (since just a little while back there were issues with
crash handling in subs..) when it comes to crash-REPLing.
2025-06-17 14:44:30 -04:00
Tyler Goodlet df8e326e39 Add `debugging/subactor_bp_in_ctx.py` test set
It's been in the debug scripts quite a while without a wrapping test and
will be,
- only the 2nd such REPL test which uses a lower-level `@context` ep-API
- the first official and explicit use of `enable_transports=['uds']`
  a suite.

Deats,
- flip to 'uds' tpt and 'devx' level logging in the script.
- add a new 2-case suite `test_ctxep_pauses_n_maybe_ipc_breaks` which
  validates both the quit-early (via `BdbQuit`) and
  channel-dropped-need-to-ctlc cases from a single test fn.
2025-06-17 14:29:01 -04:00
Tyler Goodlet 13dbd1d420 Enforce named-args only to `.open_nursery()` 2025-06-17 12:31:36 -04:00
Tyler Goodlet b2c415c4f6 Hide `._rpc._errors_relayed_via_ipc()` frame by def 2025-06-17 12:30:59 -04:00
10 changed files with 152 additions and 51 deletions

View File

@ -24,10 +24,9 @@ async def spawn_until(depth=0):
async def main(): async def main():
"""The main ``tractor`` routine. '''
The process tree should look as approximately as follows when the
The process tree should look as approximately as follows when the debugger debugger first engages:
first engages:
python examples/debugging/multi_nested_subactors_bp_forever.py python examples/debugging/multi_nested_subactors_bp_forever.py
python -m tractor._child --uid ('spawner1', '7eab8462 ...) python -m tractor._child --uid ('spawner1', '7eab8462 ...)
@ -37,10 +36,11 @@ async def main():
python -m tractor._child --uid ('spawner0', '1d42012b ...) python -m tractor._child --uid ('spawner0', '1d42012b ...)
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,
loglevel='warning' loglevel='devx',
enable_transports=['uds'],
) as n: ) as n:
# spawn both actors # spawn both actors

View File

@ -37,6 +37,7 @@ async def main(
enable_stack_on_sig=True, enable_stack_on_sig=True,
# maybe_enable_greenback=False, # maybe_enable_greenback=False,
loglevel='devx', loglevel='devx',
enable_transports=['uds'],
) as an, ) as an,
): ):
ptl: tractor.Portal = await an.start_actor( ptl: tractor.Portal = await an.start_actor(

View File

@ -33,8 +33,11 @@ async def just_bp(
async def main(): async def main():
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
enable_transports=['uds'],
loglevel='devx',
) as n: ) as n:
p = await n.start_actor( p = await n.start_actor(
'bp_boi', 'bp_boi',

View File

@ -10,10 +10,14 @@ TODO:
- wonder if any of it'll work on OS X? - wonder if any of it'll work on OS X?
""" """
from __future__ import annotations
from functools import partial from functools import partial
import itertools import itertools
import platform import platform
import time import time
from typing import (
TYPE_CHECKING,
)
import pytest import pytest
from pexpect.exceptions import ( from pexpect.exceptions import (
@ -34,6 +38,9 @@ from .conftest import (
assert_before, assert_before,
) )
if TYPE_CHECKING:
from ..conftest import PexpectSpawner
# TODO: The next great debugger audit could be done by you! # TODO: The next great debugger audit could be done by you!
# - recurrent entry to breakpoint() from single actor *after* and an # - recurrent entry to breakpoint() from single actor *after* and an
# error in another task? # error in another task?
@ -1062,6 +1069,88 @@ def test_shield_pause(
child.expect(EOF) child.expect(EOF)
@pytest.mark.parametrize(
'quit_early', [False, True]
)
def test_ctxep_pauses_n_maybe_ipc_breaks(
spawn: PexpectSpawner,
quit_early: bool,
):
'''
Audit generator embedded `.pause()`es from within a `@context`
endpoint with a chan close at the end, requiring that ctl-c is
mashed and zombie reaper kills sub with no hangs.
'''
child = spawn('subactor_bp_in_ctx')
child.expect(PROMPT)
# 3 iters for the `gen()` pause-points
for i in range(3):
assert_before(
child,
[
_pause_msg,
"('bp_boi'", # actor name
"<Task 'just_bp'", # task name
]
)
if (
i == 1
and
quit_early
):
child.sendline('q')
child.expect(PROMPT)
assert_before(
child,
["tractor._exceptions.RemoteActorError: remote task raised a 'BdbQuit'",
"bdb.BdbQuit",
"('bp_boi'",
]
)
child.sendline('c')
child.expect(EOF)
assert_before(
child,
["tractor._exceptions.RemoteActorError: remote task raised a 'BdbQuit'",
"bdb.BdbQuit",
"('bp_boi'",
]
)
break # end-of-test
child.sendline('c')
try:
child.expect(PROMPT)
except TIMEOUT:
# no prompt since we hang due to IPC chan purposely
# closed so verify we see error reporting as well as
# a failed crash-REPL request msg and can CTL-c our way
# out.
assert_before(
child,
['peer IPC channel closed abruptly?',
'another task closed this fd',
'Debug lock request was CANCELLED?',
"TransportClosed: 'MsgpackUDSStream' was already closed locally ?",]
# XXX races on whether these show/hit?
# 'Failed to REPl via `_pause()` You called `tractor.pause()` from an already cancelled scope!',
# 'AssertionError',
)
# OSc(ancel) the hanging tree
do_ctlc(
child=child,
expect_prompt=False,
)
child.expect(EOF)
assert_before(
child,
['KeyboardInterrupt'],
)
# TODO: better error for "non-ideal" usage from the root actor. # TODO: better error for "non-ideal" usage from the root actor.
# -[ ] if called from an async scope emit a message that suggests # -[ ] if called from an async scope emit a message that suggests
# using `await tractor.pause()` instead since it's less overhead # using `await tractor.pause()` instead since it's less overhead

View File

@ -135,12 +135,12 @@ def _trio_main(
f' loglevel: {actor.loglevel}\n' f' loglevel: {actor.loglevel}\n'
) )
log.info( log.info(
'Starting new `trio` subactor:\n' 'Starting new `trio` subactor\n'
+ +
pformat.nest_from_op( pformat.nest_from_op(
input_op='>(', # see syntax ideas above input_op='>(', # see syntax ideas above
tree_str=actor_info, text=actor_info,
back_from_op=2, # since "complete" nest_indent=2, # since "complete"
) )
) )
logmeth = log.info logmeth = log.info
@ -149,8 +149,8 @@ def _trio_main(
+ +
pformat.nest_from_op( pformat.nest_from_op(
input_op=')>', # like a "closed-to-play"-icon from super perspective input_op=')>', # like a "closed-to-play"-icon from super perspective
tree_str=actor_info, text=actor_info,
back_from_op=1, nest_indent=1,
) )
) )
try: try:
@ -167,7 +167,7 @@ def _trio_main(
+ +
pformat.nest_from_op( pformat.nest_from_op(
input_op='c)>', # closed due to cancel (see above) input_op='c)>', # closed due to cancel (see above)
tree_str=actor_info, text=actor_info,
) )
) )
except BaseException as err: except BaseException as err:
@ -177,7 +177,7 @@ def _trio_main(
+ +
pformat.nest_from_op( pformat.nest_from_op(
input_op='x)>', # closed by error input_op='x)>', # closed by error
tree_str=actor_info, text=actor_info,
) )
) )
# NOTE since we raise a tb will already be shown on the # NOTE since we raise a tb will already be shown on the

View File

@ -521,7 +521,7 @@ async def open_root_actor(
op_nested_actor_repr: str = _pformat.nest_from_op( op_nested_actor_repr: str = _pformat.nest_from_op(
input_op='>) ', input_op='>) ',
tree_str=actor.pformat(), text=actor.pformat(),
nest_prefix='|_', nest_prefix='|_',
) )
logger.info( logger.info(

View File

@ -255,7 +255,7 @@ async def _errors_relayed_via_ipc(
ctx: Context, ctx: Context,
is_rpc: bool, is_rpc: bool,
hide_tb: bool = False, hide_tb: bool = True,
debug_kbis: bool = False, debug_kbis: bool = False,
task_status: TaskStatus[ task_status: TaskStatus[
Context | BaseException Context | BaseException
@ -380,9 +380,9 @@ async def _errors_relayed_via_ipc(
# they can be individually ccancelled. # they can be individually ccancelled.
finally: finally:
# if the error is not from user code and instead a failure # if the error is not from user code and instead a failure of
# of a runtime RPC or transport failure we do prolly want to # an internal-runtime-RPC or IPC-connection, we do (prolly) want
# show this frame # to show this frame!
if ( if (
rpc_err rpc_err
and ( and (

View File

@ -1699,9 +1699,9 @@ async def async_main(
op_nested_actor_repr: str = _pformat.nest_from_op( op_nested_actor_repr: str = _pformat.nest_from_op(
input_op=')>', input_op=')>',
tree_str=actor.pformat(), text=actor.pformat(),
nest_prefix='|_', nest_prefix='|_',
back_from_op=2, nest_indent=1, # under >
) )
teardown_report += ( teardown_report += (
'Actor runtime exited\n' 'Actor runtime exited\n'

View File

@ -593,9 +593,10 @@ async def _open_and_supervise_one_cancels_all_nursery(
# final exit # final exit
@acm
# @api_frame # @api_frame
@acm
async def open_nursery( async def open_nursery(
*, # named params only!
hide_tb: bool = True, hide_tb: bool = True,
**kwargs, **kwargs,
# ^TODO, paramspec for `open_root_actor()` # ^TODO, paramspec for `open_root_actor()`

View File

@ -249,14 +249,29 @@ def pformat_cs(
) )
# TODO: move this func to some kinda `.devx.pformat.py` eventually
# as we work out our multi-domain state-flow-syntax!
def nest_from_op( def nest_from_op(
input_op: str, input_op: str, # TODO, Literal of all op-"symbols" from below?
text: str, # TODO? better name, like `text`?
nest_prefix: str = '|_',
nest_indent: int = 0,
# XXX indent `next_prefix` "to-the-right-of" `input_op`
# by this count of whitespaces (' ').
) -> str:
'''
Depth-increment the input (presumably hierarchy/supervision)
input "tree string" below the provided `input_op` execution
operator, so injecting a `"\n|_{input_op}\n"`and indenting the
`tree_str` to nest content aligned with the ops last char.
'''
# `sclang` "structurred-concurrency-language": an ascii-encoded
# symbolic alphabet to describe concurrent systems.
# #
# ?TODO? an idea for a syntax to the state of concurrent systems # ?TODO? aa more fomal idea for a syntax to the state of
# as a "3-domain" (execution, scope, storage) model and using # concurrent systems as a "3-domain" (execution, scope, storage)
# a minimal ascii/utf-8 operator-set. # model and using a minimal ascii/utf-8 operator-set.
# #
# try not to take any of this seriously yet XD # try not to take any of this seriously yet XD
# #
@ -322,37 +337,29 @@ def nest_from_op(
# #
# =>{ recv-req to open # =>{ recv-req to open
# <={ send-status that it closed # <={ send-status that it closed
#
if (
nest_prefix
and
nest_indent
):
nest_prefix: str = textwrap.indent(
nest_prefix,
prefix=nest_indent*' ',
)
tree_str: str, tree_str_indent: int = len(nest_prefix)
# NOTE: so move back-from-the-left of the `input_op` by
# this amount.
back_from_op: int = 0,
nest_prefix: str = ''
) -> str:
'''
Depth-increment the input (presumably hierarchy/supervision)
input "tree string" below the provided `input_op` execution
operator, so injecting a `"\n|_{input_op}\n"`and indenting the
`tree_str` to nest content aligned with the ops last char.
'''
indented_tree_str: str = textwrap.indent( indented_tree_str: str = textwrap.indent(
tree_str, text,
prefix=' ' *( prefix=' '*tree_str_indent
len(input_op)
-
(back_from_op + 1)
),
) )
# inject any provided nesting-prefix chars # inject any provided nesting-prefix chars
# into the head of the first line. # into the head of the first line.
if nest_prefix: if nest_prefix:
indented_tree_str: str = ( indented_tree_str: str = (
f'{nest_prefix}' f'{nest_prefix}{indented_tree_str[tree_str_indent:]}'
f'{indented_tree_str[len(nest_prefix):]}'
) )
return ( return (
f'{input_op}\n' f'{input_op}\n'
f'{indented_tree_str}' f'{indented_tree_str}'