Compare commits

..

No commits in common. "e4ec6b7b0c3401b336e03a4ea7b42015480b7677" and "3538ccd7992282a1b81e3560282a4e03b9e667d2" have entirely different histories.

9 changed files with 136 additions and 288 deletions

View File

@ -146,10 +146,9 @@ def in_prompt_msg(
log/REPL output for a given `pdb` interact point.
'''
__tracebackhide__: bool = False
for part in parts:
if part not in prompt:
if pause_on_false:
import pdbp
pdbp.set_trace()
@ -168,7 +167,6 @@ def assert_before(
**kwargs,
) -> None:
__tracebackhide__: bool = False
# as in before the prompt end
before: str = str(child.before.decode())
@ -221,10 +219,7 @@ def ctlc(
],
ids=lambda item: f'{item[0]} -> {item[1]}',
)
def test_root_actor_error(
spawn,
user_in_out,
):
def test_root_actor_error(spawn, user_in_out):
'''
Demonstrate crash handler entering pdb from basic error in root actor.
@ -470,12 +465,8 @@ def test_subactor_breakpoint(
child.expect(PROMPT)
before = str(child.before.decode())
assert in_prompt_msg(
before,
['RemoteActorError:',
"('breakpoint_forever'",
'bdb.BdbQuit',]
)
assert "RemoteActorError: ('breakpoint_forever'" in before
assert 'bdb.BdbQuit' in before
if ctlc:
do_ctlc(child)
@ -487,12 +478,8 @@ def test_subactor_breakpoint(
child.expect(pexpect.EOF)
before = str(child.before.decode())
assert in_prompt_msg(
before,
['RemoteActorError:',
"('breakpoint_forever'",
'bdb.BdbQuit',]
)
assert "RemoteActorError: ('breakpoint_forever'" in before
assert 'bdb.BdbQuit' in before
@has_nested_actors
@ -760,9 +747,8 @@ def test_multi_daemon_subactors(
# boxed error raised in root task
# "Attaching to pdb in crashed actor: ('root'",
_crash_msg,
"('root'", # should attach in root
"_exceptions.RemoteActorError:", # with an embedded RAE for..
"('name_error'", # the src subactor which raised
"('root'",
"_exceptions.RemoteActorError: ('name_error'",
]
)
@ -863,11 +849,10 @@ def test_multi_nested_subactors_error_through_nurseries(
# https://github.com/goodboy/tractor/issues/320
# ctlc: bool,
):
'''
Verify deeply nested actors that error trigger debugger entries
"""Verify deeply nested actors that error trigger debugger entries
at each actor nurserly (level) all the way up the tree.
'''
"""
# NOTE: previously, inside this script was a bug where if the
# parent errors before a 2-levels-lower actor has released the lock,
# the parent tries to cancel it but it's stuck in the debugger?
@ -887,31 +872,22 @@ def test_multi_nested_subactors_error_through_nurseries(
except EOF:
break
assert_before(
child,
[ # boxed source errors
"NameError: name 'doggypants' is not defined",
"tractor._exceptions.RemoteActorError:",
"('name_error'",
"bdb.BdbQuit",
assert_before(child, [
# first level subtrees
# "tractor._exceptions.RemoteActorError: ('spawner0'",
"src_uid=('spawner0'",
# boxed source errors
"NameError: name 'doggypants' is not defined",
"tractor._exceptions.RemoteActorError: ('name_error'",
"bdb.BdbQuit",
# "tractor._exceptions.RemoteActorError: ('spawner1'",
# first level subtrees
"tractor._exceptions.RemoteActorError: ('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_0'",
"relay_uid=('spawn_until_1'",
"src_uid=('spawn_until_2'",
]
)
# 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'",
])
@pytest.mark.timeout(15)
@ -1045,16 +1021,13 @@ def test_different_debug_mode_per_actor(
# msg reported back from the debug mode actor is processed.
# assert "tractor._exceptions.RemoteActorError: ('debugged_boi'" in before
assert "tractor._exceptions.RemoteActorError: ('crash_boi'" in before
# the crash boi should not have made a debugger request but
# instead crashed completely
assert_before(
child,
[
"tractor._exceptions.RemoteActorError:",
"src_uid=('crash_boi'",
"RuntimeError",
]
)
assert "tractor._exceptions.RemoteActorError: ('crash_boi'" in before
assert "RuntimeError" in before
def test_pause_from_sync(
@ -1073,15 +1046,13 @@ def test_pause_from_sync(
assert_before(
child,
[
'`greenback` portal opened!',
# pre-prompt line
_pause_msg,
"<Task '__main__.main'",
"('root'",
_pause_msg, "('root'",
]
)
if ctlc:
do_ctlc(child)
child.sendline('c')
child.expect(PROMPT)
@ -1098,7 +1069,6 @@ def test_pause_from_sync(
if ctlc:
do_ctlc(child)
child.sendline('c')
child.expect(PROMPT)
assert_before(
@ -1108,7 +1078,6 @@ def test_pause_from_sync(
if ctlc:
do_ctlc(child)
child.sendline('c')
child.expect(PROMPT)
# non-main thread case
@ -1120,22 +1089,5 @@ def test_pause_from_sync(
if ctlc:
do_ctlc(child)
child.sendline('c')
child.expect(pexpect.EOF)
# TODO!
def test_correct_frames_below_hidden():
'''
Ensure that once a `tractor.pause()` enages, when the user
inputs a "next"/"n" command the actual next line steps
and that using a "step"/"s" into the next LOC, particuarly
`tractor` APIs, you can step down into that code.
'''
...
def test_cant_pause_from_paused_task():
...

View File

@ -33,7 +33,6 @@ from .log import (
get_logger,
)
from . import _state
from .devx import _debug
from .to_asyncio import run_as_asyncio_guest
from ._runtime import (
async_main,
@ -97,6 +96,7 @@ def _mp_main(
def _trio_main(
actor: Actor,
*,
parent_addr: tuple[str, int] | None = None,
@ -107,9 +107,7 @@ def _trio_main(
Entry point for a `trio_run_in_process` subactor.
'''
# __tracebackhide__: bool = True
_debug.hide_runtime_frames()
__tracebackhide__: bool = True
_state._current_actor = actor
trio_main = partial(
async_main,
@ -148,6 +146,7 @@ def _trio_main(
+
actor_info
)
finally:
log.info(
'Subactor terminated\n'

View File

@ -35,6 +35,7 @@ import trio
from msgspec import (
defstruct,
msgpack,
Raw,
structs,
ValidationError,
)
@ -43,12 +44,11 @@ from tractor._state import current_actor
from tractor.log import get_logger
from tractor.msg import (
Error,
PayloadMsg,
MsgType,
MsgCodec,
MsgDec,
Stop,
types as msgtypes,
MsgCodec,
MsgDec,
)
from tractor.msg.pretty_struct import (
iter_fields,
@ -156,7 +156,6 @@ def pack_from_raise(
`Error`-msg using `pack_error()` to extract the tb info.
'''
__tracebackhide__: bool = True
try:
raise local_err
except type(local_err) as local_err:
@ -526,26 +525,10 @@ class RemoteActorError(Exception):
if not with_type_header:
body = '\n' + body
else:
first: str = ''
message: str = self._message
# split off the first line so it isn't indented
# the same like the "boxed content".
if not with_type_header:
lines: list[str] = message.splitlines()
first = lines[0]
message = ''.join(lines[1:])
body: str = (
first
+
textwrap.indent(
message,
prefix=' ',
)
+
'\n'
)
body: str = textwrap.indent(
self._message,
prefix=' ',
) + '\n'
if with_type_header:
tail: str = ')>'
@ -751,38 +734,25 @@ class MsgTypeError(
def from_decode(
cls,
message: str,
ipc_msg: PayloadMsg|None = None,
msgdict: dict|None = None,
msgdict: dict,
) -> MsgTypeError:
'''
Constuctor for easy creation from (presumably) catching
the backend interchange lib's underlying validation error
and passing context-specific meta-data to `_mk_msg_type_err()`
(which is normally the caller of this).
'''
# if provided, expand and pack all RAE compat fields into the
# `._extra_msgdata` auxillary data `dict` internal to
# `RemoteActorError`.
extra_msgdata: dict = {}
if msgdict:
extra_msgdata: dict = {
k: v
for k, v in msgdict.items()
if k in _ipcmsg_keys
}
# NOTE: original "vanilla decode" of the msg-bytes
# is placed inside a value readable from
# `.msgdata['_msg_dict']`
extra_msgdata['_msg_dict'] = msgdict
return cls(
message=message,
boxed_type=cls,
ipc_msg=ipc_msg,
**extra_msgdata,
# NOTE: original "vanilla decode" of the msg-bytes
# is placed inside a value readable from
# `.msgdata['_msg_dict']`
_msg_dict=msgdict,
# expand and pack all RAE compat fields
# into the `._extra_msgdata` aux `dict`.
**{
k: v
for k, v in msgdict.items()
if k in _ipcmsg_keys
},
)
@ -1106,7 +1076,7 @@ _raise_from_no_key_in_msg = _raise_from_unexpected_msg
def _mk_msg_type_err(
msg: Any|bytes|MsgType,
msg: Any|bytes|Raw,
codec: MsgCodec|MsgDec,
message: str|None = None,
@ -1115,7 +1085,6 @@ def _mk_msg_type_err(
src_validation_error: ValidationError|None = None,
src_type_error: TypeError|None = None,
is_invalid_payload: bool = False,
src_err_msg: Error|None = None,
**mte_kwargs,
@ -1190,10 +1159,9 @@ def _mk_msg_type_err(
# only the payload being wrong?
# -[ ] maybe the better design is to break this construct
# logic into a separate explicit helper raiser-func?
msg_dict = None
msg_dict: dict = {}
else:
msg: bytes
# decode the msg-bytes using the std msgpack
# interchange-prot (i.e. without any
# `msgspec.Struct` handling) so that we can
@ -1238,14 +1206,6 @@ def _mk_msg_type_err(
msgtyperr = MsgTypeError.from_decode(
message=message,
msgdict=msg_dict,
# NOTE: for the send-side `.started()` pld-validate
# case we actually set the `._ipc_msg` AFTER we return
# from here inside `Context.started()` since we actually
# want to emulate the `Error` from the mte we build here
# Bo
# so by default in that case this is set to `None`
ipc_msg=src_err_msg,
)
msgtyperr.__cause__ = src_validation_error
return msgtyperr

View File

@ -91,16 +91,12 @@ async def open_root_actor(
# and that this call creates it.
ensure_registry: bool = False,
hide_tb: bool = True,
) -> Actor:
'''
Runtime init entry point for ``tractor``.
'''
__tracebackhide__: bool = hide_tb
_debug.hide_runtime_frames()
__tracebackhide__ = True
# TODO: stick this in a `@cm` defined in `devx._debug`?
#
# Override the global debugger hook to make it play nice with
@ -129,7 +125,7 @@ async def open_root_actor(
# usage by a clobbered TTY's stdstreams!
def block_bps(*args, **kwargs):
raise RuntimeError(
'Trying to use `breakpoint()` eh?\n\n'
'Trying to use `breakpoint()` eh?\n'
'Welp, `tractor` blocks `breakpoint()` built-in calls by default!\n'
'If you need to use it please install `greenback` and set '
'`debug_mode=True` when opening the runtime '
@ -137,9 +133,7 @@ async def open_root_actor(
)
sys.breakpointhook = block_bps
# lol ok,
# https://docs.python.org/3/library/sys.html#sys.breakpointhook
os.environ['PYTHONBREAKPOINT'] = "0"
# os.environ['PYTHONBREAKPOINT'] = None
# attempt to retreive ``trio``'s sigint handler and stash it
# on our debugger lock state.
@ -209,7 +203,6 @@ async def open_root_actor(
):
loglevel = 'PDB'
elif debug_mode:
raise RuntimeError(
"Debug mode is only supported for the `trio` backend!"

View File

@ -692,21 +692,21 @@ class Actor:
proc: trio.Process
_, proc, _ = entry
if (
(poll := getattr(proc, 'poll', None))
and poll() is None
):
log.cancel(
'Root actor reports no-more-peers, BUT\n'
'a DISCONNECTED child still has the debug '
'lock!\n\n'
# f'root uid: {self.uid}\n'
f'last disconnected child uid: {uid}\n'
f'locking child uid: {pdb_user_uid}\n'
)
await maybe_wait_for_debugger(
child_in_debug=True
)
if (
(poll := getattr(proc, 'poll', None))
and poll() is None
):
log.cancel(
'Root actor reports no-more-peers, BUT\n'
'a DISCONNECTED child still has the debug '
'lock!\n\n'
# f'root uid: {self.uid}\n'
f'last disconnected child uid: {uid}\n'
f'locking child uid: {pdb_user_uid}\n'
)
await maybe_wait_for_debugger(
child_in_debug=True
)
# TODO: just bc a child's transport dropped
# doesn't mean it's not still using the pdb
@ -1140,6 +1140,7 @@ class Actor:
requester_type,
req_chan,
log_meth,
) = (
req_chan.uid,
'peer',
@ -1172,11 +1173,7 @@ class Actor:
# with the root actor in this tree
debug_req = _debug.DebugStatus
lock_req_ctx: Context = debug_req.req_ctx
if (
lock_req_ctx
and
lock_req_ctx.has_outcome
):
if lock_req_ctx is not None:
msg += (
'-> Cancelling active debugger request..\n'
f'|_{_debug.Lock.repr()}\n\n'

View File

@ -43,7 +43,6 @@ from tractor._state import (
is_main_process,
is_root_process,
debug_mode,
_runtime_vars,
)
from tractor.log import get_logger
from tractor._portal import Portal
@ -300,6 +299,7 @@ async def hard_kill(
async def soft_kill(
proc: ProcessType,
wait_func: Callable[
[ProcessType],
@ -329,18 +329,6 @@ async def soft_kill(
await wait_func(proc)
except trio.Cancelled:
with trio.CancelScope(shield=True):
await maybe_wait_for_debugger(
child_in_debug=_runtime_vars.get(
'_debug_mode', False
),
header_msg=(
'Delaying `soft_kill()` subproc reaper while debugger locked..\n'
),
# TODO: need a diff value then default?
# poll_steps=9999999,
)
# if cancelled during a soft wait, cancel the child
# actor before entering the hard reap sequence
# below. This means we try to do a graceful teardown

View File

@ -48,11 +48,9 @@ from typing import (
TYPE_CHECKING,
)
from types import (
FunctionType,
FrameType,
ModuleType,
TracebackType,
CodeType,
)
from msgspec import Struct
@ -92,72 +90,43 @@ if TYPE_CHECKING:
log = get_logger(__name__)
# XXX HACKZONE XXX
# hide exit stack frames on nurseries and cancel-scopes!
# |_ so avoid seeing it when the `pdbp` REPL is first engaged from
# inside a `trio.open_nursery()` scope (with no line after it
# in before the block end??).
#
# TODO: FINALLY got this workin originally with
# `@pdbp.hideframe` around the `wrapper()` def embedded inside
# `_ki_protection_decoratior()`.. which is in the module:
# /home/goodboy/.virtualenvs/tractor311/lib/python3.11/site-packages/trio/_core/_ki.py
#
# -[ ] make an issue and patch for `trio` core? maybe linked
# to the long outstanding `pdb` one below?
# |_ it's funny that there's frame hiding throughout `._run.py`
# but not where it matters on the below exit funcs..
#
# -[ ] provide a patchset for the lonstanding
# |_ https://github.com/python-trio/trio/issues/1155
#
# -[ ] make a linked issue to ^ and propose allowing all the
# `._core._run` code to have their `__tracebackhide__` value
# configurable by a `RunVar` to allow getting scheduler frames
# if desired through configuration?
#
# -[ ] maybe dig into the core `pdb` issue why the extra frame is shown
# at all?
#
pdbp.hideframe(trio._core._run.NurseryManager.__aexit__)
pdbp.hideframe(trio._core._run.CancelScope.__exit__)
pdbp.hideframe(_GeneratorContextManager.__exit__)
pdbp.hideframe(_AsyncGeneratorContextManager.__aexit__)
pdbp.hideframe(trio.Event.wait)
def hide_runtime_frames() -> dict[FunctionType, CodeType]:
'''
Hide call-stack frames for various std-lib and `trio`-API primitives
such that the tracebacks presented from our runtime are as minimized
as possible, particularly from inside a `PdbREPL`.
'''
# XXX HACKZONE XXX
# hide exit stack frames on nurseries and cancel-scopes!
# |_ so avoid seeing it when the `pdbp` REPL is first engaged from
# inside a `trio.open_nursery()` scope (with no line after it
# in before the block end??).
#
# TODO: FINALLY got this workin originally with
# `@pdbp.hideframe` around the `wrapper()` def embedded inside
# `_ki_protection_decoratior()`.. which is in the module:
# /home/goodboy/.virtualenvs/tractor311/lib/python3.11/site-packages/trio/_core/_ki.py
#
# -[ ] make an issue and patch for `trio` core? maybe linked
# to the long outstanding `pdb` one below?
# |_ it's funny that there's frame hiding throughout `._run.py`
# but not where it matters on the below exit funcs..
#
# -[ ] provide a patchset for the lonstanding
# |_ https://github.com/python-trio/trio/issues/1155
#
# -[ ] make a linked issue to ^ and propose allowing all the
# `._core._run` code to have their `__tracebackhide__` value
# configurable by a `RunVar` to allow getting scheduler frames
# if desired through configuration?
#
# -[ ] maybe dig into the core `pdb` issue why the extra frame is shown
# at all?
#
funcs: list[FunctionType] = [
trio._core._run.NurseryManager.__aexit__,
trio._core._run.CancelScope.__exit__,
_GeneratorContextManager.__exit__,
_AsyncGeneratorContextManager.__aexit__,
_AsyncGeneratorContextManager.__aenter__,
trio.Event.wait,
]
func_list_str: str = textwrap.indent(
"\n".join(f.__qualname__ for f in funcs),
prefix=' |_ ',
)
log.devx(
'Hiding the following runtime frames by default:\n'
f'{func_list_str}\n'
)
codes: dict[FunctionType, CodeType] = {}
for ref in funcs:
# stash a pre-modified version of each ref's code-obj
# so it can be reverted later if needed.
codes[ref] = ref.__code__
pdbp.hideframe(ref)
#
# pdbp.hideframe(trio._core._run.NurseryManager.__aexit__)
# pdbp.hideframe(trio._core._run.CancelScope.__exit__)
# pdbp.hideframe(_GeneratorContextManager.__exit__)
# pdbp.hideframe(_AsyncGeneratorContextManager.__aexit__)
# pdbp.hideframe(_AsyncGeneratorContextManager.__aenter__)
# pdbp.hideframe(trio.Event.wait)
return codes
__all__ = [
'breakpoint',
'post_mortem',
]
class LockStatus(
@ -1063,24 +1032,15 @@ async def request_root_stdio_lock(
except (
BaseException,
) as ctx_err:
message: str = (
'Failed during debug request dialog with root actor?\n\n'
):
log.exception(
'Failed during root TTY-lock dialog?\n'
f'{req_ctx}\n'
f'Cancelling IPC ctx!\n'
)
if req_ctx:
message += (
f'{req_ctx}\n'
f'Cancelling IPC ctx!\n'
)
await req_ctx.cancel()
else:
message += 'Failed during `Portal.open_context()` ?\n'
log.exception(message)
ctx_err.add_note(message)
raise ctx_err
await req_ctx.cancel()
raise
except (
@ -1107,7 +1067,6 @@ async def request_root_stdio_lock(
# ctl-c out of the currently hanging task!
raise DebugRequestError(
'Failed to lock stdio from subactor IPC ctx!\n\n'
f'req_ctx: {DebugStatus.req_ctx}\n'
) from req_err
@ -1818,7 +1777,7 @@ def _set_trace(
):
__tracebackhide__: bool = hide_tb
actor: tractor.Actor = actor or current_actor()
task: trio.Task = task or current_task()
task: task or current_task()
# else:
# TODO: maybe print the actor supervion tree up to the

View File

@ -53,14 +53,17 @@ LOG_FORMAT = (
DATE_FORMAT = '%b %d %H:%M:%S'
# FYI, ERROR is 40
CUSTOM_LEVELS: dict[str, int] = {
LEVELS: dict[str, int] = {
'TRANSPORT': 5,
'RUNTIME': 15,
'CANCEL': 16,
'DEVX': 17,
'DEVX': 400,
'PDB': 500,
}
# _custom_levels: set[str] = {
# lvlname.lower for lvlname in LEVELS.keys()
# }
STD_PALETTE = {
'CRITICAL': 'red',
'ERROR': 'red',
@ -134,7 +137,7 @@ class StackLevelAdapter(LoggerAdapter):
"Developer experience" sub-sys statuses.
'''
return self.log(17, msg)
return self.log(400, msg)
def log(
self,
@ -151,7 +154,8 @@ class StackLevelAdapter(LoggerAdapter):
if self.isEnabledFor(level):
stacklevel: int = 3
if (
level in CUSTOM_LEVELS.values()
level in LEVELS.values()
# or level in _custom_levels
):
stacklevel: int = 4
@ -198,8 +202,7 @@ class StackLevelAdapter(LoggerAdapter):
)
# TODO IDEAs:
# -[ ] move to `.devx.pformat`?
# TODO IDEA:
# -[ ] do per task-name and actor-name color coding
# -[ ] unique color per task-id and actor-uuid
def pformat_task_uid(
@ -306,7 +309,7 @@ def get_logger(
logger = StackLevelAdapter(log, ActorContextInfo())
# additional levels
for name, val in CUSTOM_LEVELS.items():
for name, val in LEVELS.items():
logging.addLevelName(val, name)
# ensure customs levels exist as methods
@ -374,7 +377,7 @@ def at_least_level(
'''
if isinstance(level, str):
level: int = CUSTOM_LEVELS[level.upper()]
level: int = LEVELS[level.upper()]
if log.getEffectiveLevel() <= level:
return True

View File

@ -162,10 +162,7 @@ class MsgDec(Struct):
# TODO: would get moved into `FieldSpec.__str__()` right?
@property
def spec_str(self) -> str:
return pformat_msgspec(
codec=self,
join_char='|',
)
return pformat_msgspec(codec=self)
pld_spec_str = spec_str
@ -214,7 +211,7 @@ def mk_msgspec_table(
msgtypes = [msgspec]
msgt_table: dict[str, MsgType] = {
msgt: str(msgt.__name__)
msgt: str(msgt)
for msgt in msgtypes
}
if msg: