Better RAE `.pformat()`-ing for send-side MTEs

Send-side `MsgTypeError`s actually shouldn't have any "boxed" traceback
per say since they're raised in the transmitting actor's local task env
and we (normally) don't want the ascii decoration added around the
error's `._message: str`, that is not until the exc is `pack_error()`-ed
before transit. As such, the presentation of an embedded traceback (and
its ascii box) gets bypassed when only a `._message: str` is set (as we
now do for pld-spec failures in `_mk_msg_type_err()`).

Further this tweaks the `.pformat()` output to include the `._message`
part to look like `<RemoteActorError( <._message> ) ..` instead of
jamming it implicitly to the end of the embedded `.tb_str` (as was done
implicitly by `unpack_error()`) and also adds better handling for the
`with_type_header == False` case including forcing that case when we
detect that the currently handled exc is the RAE in `.pformat()`.
Toss in a lengthier doc-str explaining it all.

Surrounding/supporting changes,
- better `unpack_error()` message which just briefly reports the remote
  task's error type.
- add public `.message: str` prop.
- always set a `._extra_msgdata: dict` since some MTE props rely on it.
- handle `.boxed_type == None` for `.boxed_type_str`.
- maybe pack any detected input or `exc.message` in `pack_error()`.
- comment cruft cleanup in `_mk_msg_type_err()`.
runtime_to_msgspec
Tyler Goodlet 2024-05-30 10:04:54 -04:00
parent 1db5d4def2
commit 0e8c60ee4a
1 changed files with 124 additions and 75 deletions

View File

@ -22,6 +22,7 @@ from __future__ import annotations
import builtins import builtins
import importlib import importlib
from pprint import pformat from pprint import pformat
import sys
from types import ( from types import (
TracebackType, TracebackType,
) )
@ -110,6 +111,7 @@ _body_fields: list[str] = list(
'tb_str', 'tb_str',
'relay_path', 'relay_path',
'cid', 'cid',
'message',
# only ctxc should show it but `Error` does # only ctxc should show it but `Error` does
# have it as an optional field. # have it as an optional field.
@ -236,6 +238,7 @@ class RemoteActorError(Exception):
self._boxed_type: BaseException = boxed_type self._boxed_type: BaseException = boxed_type
self._src_type: BaseException|None = None self._src_type: BaseException|None = None
self._ipc_msg: Error|None = ipc_msg self._ipc_msg: Error|None = ipc_msg
self._extra_msgdata = extra_msgdata
if ( if (
extra_msgdata extra_msgdata
@ -250,8 +253,6 @@ class RemoteActorError(Exception):
k, k,
v, v,
) )
else:
self._extra_msgdata = extra_msgdata
# TODO: mask out eventually or place in `pack_error()` # TODO: mask out eventually or place in `pack_error()`
# pre-`return` lines? # pre-`return` lines?
@ -282,6 +283,17 @@ class RemoteActorError(Exception):
# ensure any roundtripping evals to the input value # ensure any roundtripping evals to the input value
assert self.boxed_type is boxed_type assert self.boxed_type is boxed_type
@property
def message(self) -> str:
'''
Be explicit, instead of trying to read it from the the parent
type's loosely defined `.args: tuple`:
https://docs.python.org/3/library/exceptions.html#BaseException.args
'''
return self._message
@property @property
def ipc_msg(self) -> Struct: def ipc_msg(self) -> Struct:
''' '''
@ -355,7 +367,10 @@ class RemoteActorError(Exception):
''' '''
bt: Type[BaseException] = self.boxed_type bt: Type[BaseException] = self.boxed_type
return str(bt.__name__) if bt:
return str(bt.__name__)
return ''
@property @property
def boxed_type(self) -> Type[BaseException]: def boxed_type(self) -> Type[BaseException]:
@ -426,8 +441,7 @@ class RemoteActorError(Exception):
for key in fields: for key in fields:
if ( if (
key == 'relay_uid' key == 'relay_uid' and not self.is_inception()
and not self.is_inception()
): ):
continue continue
@ -504,19 +518,80 @@ class RemoteActorError(Exception):
def pformat( def pformat(
self, self,
with_type_header: bool = True, with_type_header: bool = True,
# with_ascii_box: bool = True,
) -> str: ) -> str:
''' '''
Nicely formatted boxed error meta data + traceback, OR just Format any boxed remote error by multi-line display of,
the normal message from `.args` (for eg. as you'd want shown
by a locally raised `ContextCancelled`). - error's src or relay actor meta-data,
- remote runtime env's traceback,
With optional control over the format of,
- whether the boxed traceback is ascii-decorated with
a surrounding "box" annotating the embedded stack-trace.
- if the error's type name should be added as margins
around the field and tb content like:
`<RemoteActorError(.. <<multi-line-content>> .. )>`
- the placement of the `.message: str` (explicit equiv of
`.args[0]`), either placed below the `.tb_str` or in the
first line's header when the error is raised locally (since
the type name is already implicitly shown by python).
''' '''
header: str = '' header: str = ''
body: str = '' body: str = ''
message: str = ''
# XXX when the currently raised exception is this instance,
# we do not ever use the "type header" style repr.
is_being_raised: bool = False
if (
(exc := sys.exception())
and
exc is self
):
is_being_raised: bool = True
with_type_header: bool = (
with_type_header
and
not is_being_raised
)
# <RemoteActorError( .. )> style
if with_type_header: if with_type_header:
header: str = f'<{type(self).__name__}(\n' header: str = f'<{type(self).__name__}('
if message := self._message:
# split off the first line so, if needed, it isn't
# indented the same like the "boxed content" which
# since there is no `.tb_str` is just the `.message`.
lines: list[str] = message.splitlines()
first: str = lines[0]
message: str = message.removeprefix(first)
# with a type-style header we,
# - have no special message "first line" extraction/handling
# - place the message a space in from the header:
# `MsgTypeError( <message> ..`
# ^-here
# - indent the `.message` inside the type body.
if with_type_header:
first = f' {first} )>'
message: str = textwrap.indent(
message,
prefix=' '*2,
)
message: str = first + message
# IFF there is an embedded traceback-str we always
# draw the ascii-box around it.
if tb_str := self.tb_str: if tb_str := self.tb_str:
fields: str = self._mk_fields_str( fields: str = self._mk_fields_str(
_body_fields _body_fields
@ -535,36 +610,19 @@ class RemoteActorError(Exception):
# |___ .. # |___ ..
tb_body_indent=1, tb_body_indent=1,
) )
if not with_type_header:
body = '\n' + body
elif message := self._message: tail = ''
# split off the first line so it isn't indented if (
# the same like the "boxed content". with_type_header
if not with_type_header: and not message
lines: list[str] = message.splitlines() ):
first: str = lines[0] tail: str = '>'
message: str = message.removeprefix(first)
else:
first: str = ''
body: str = (
first
+
message
+
'\n'
)
if with_type_header:
tail: str = ')>'
else:
tail = ''
return ( return (
header header
+ +
message
+
f'{body}' f'{body}'
+ +
tail tail
@ -577,7 +635,9 @@ class RemoteActorError(Exception):
# |_ i guess `pexepect` relies on `str`-casing # |_ i guess `pexepect` relies on `str`-casing
# of output? # of output?
def __str__(self) -> str: def __str__(self) -> str:
return self.pformat(with_type_header=False) return self.pformat(
with_type_header=False
)
def unwrap( def unwrap(
self, self,
@ -825,9 +885,6 @@ class MsgTypeError(
extra_msgdata['_bad_msg'] = bad_msg extra_msgdata['_bad_msg'] = bad_msg
extra_msgdata['cid'] = bad_msg.cid extra_msgdata['cid'] = bad_msg.cid
if 'cid' not in extra_msgdata:
import pdbp; pdbp.set_trace()
return cls( return cls(
message=message, message=message,
boxed_type=cls, boxed_type=cls,
@ -889,6 +946,7 @@ def pack_error(
src_uid: tuple[str, str]|None = None, src_uid: tuple[str, str]|None = None,
tb: TracebackType|None = None, tb: TracebackType|None = None,
tb_str: str = '', tb_str: str = '',
message: str = '',
) -> Error: ) -> Error:
''' '''
@ -971,7 +1029,7 @@ def pack_error(
# the locally raised error (so, NOT the prior relay's boxed # the locally raised error (so, NOT the prior relay's boxed
# `._ipc_msg.tb_str`). # `._ipc_msg.tb_str`).
error_msg['tb_str'] = tb_str error_msg['tb_str'] = tb_str
error_msg['message'] = message or getattr(exc, 'message', '')
if cid is not None: if cid is not None:
error_msg['cid'] = cid error_msg['cid'] = cid
@ -995,26 +1053,24 @@ def unpack_error(
if not isinstance(msg, Error): if not isinstance(msg, Error):
return None return None
# retrieve the remote error's msg-encoded details
tb_str: str = msg.tb_str
message: str = (
f'{chan.uid}\n'
+
tb_str
)
# try to lookup a suitable error type from the local runtime # try to lookup a suitable error type from the local runtime
# env then use it to construct a local instance. # env then use it to construct a local instance.
# boxed_type_str: str = error_dict['boxed_type_str'] # boxed_type_str: str = error_dict['boxed_type_str']
boxed_type_str: str = msg.boxed_type_str boxed_type_str: str = msg.boxed_type_str
boxed_type: Type[BaseException] = get_err_type(boxed_type_str) boxed_type: Type[BaseException] = get_err_type(boxed_type_str)
if boxed_type_str == 'ContextCancelled': # retrieve the error's msg-encoded remotoe-env info
box_type = ContextCancelled message: str = f'remote task raised a {msg.boxed_type_str!r}\n'
assert boxed_type is box_type
elif boxed_type_str == 'MsgTypeError': # TODO: do we even really need these checks for RAEs?
box_type = MsgTypeError if boxed_type_str in [
'ContextCancelled',
'MsgTypeError',
]:
box_type = {
'ContextCancelled': ContextCancelled,
'MsgTypeError': MsgTypeError,
}[boxed_type_str]
assert boxed_type is box_type assert boxed_type is box_type
# TODO: already included by `_this_mod` in else loop right? # TODO: already included by `_this_mod` in else loop right?
@ -1029,19 +1085,21 @@ def unpack_error(
exc = box_type( exc = box_type(
message, message,
ipc_msg=msg, ipc_msg=msg,
tb_str=msg.tb_str,
) )
return exc return exc
def is_multi_cancelled(exc: BaseException) -> bool: def is_multi_cancelled(
exc: BaseException|BaseExceptionGroup
) -> bool:
''' '''
Predicate to determine if a possible ``BaseExceptionGroup`` contains Predicate to determine if a possible ``BaseExceptionGroup`` contains
only ``trio.Cancelled`` sub-exceptions (and is likely the result of only ``trio.Cancelled`` sub-exceptions (and is likely the result of
cancelling a collection of subtasks. cancelling a collection of subtasks.
''' '''
# if isinstance(exc, eg.BaseExceptionGroup):
if isinstance(exc, BaseExceptionGroup): if isinstance(exc, BaseExceptionGroup):
return exc.subgroup( return exc.subgroup(
lambda exc: isinstance(exc, trio.Cancelled) lambda exc: isinstance(exc, trio.Cancelled)
@ -1184,7 +1242,6 @@ def _mk_msg_type_err(
src_validation_error: ValidationError|None = None, src_validation_error: ValidationError|None = None,
src_type_error: TypeError|None = None, src_type_error: TypeError|None = None,
is_invalid_payload: bool = False, is_invalid_payload: bool = False,
# src_err_msg: Error|None = None,
**mte_kwargs, **mte_kwargs,
@ -1251,19 +1308,11 @@ def _mk_msg_type_err(
msg_type: str = type(msg) msg_type: str = type(msg)
any_pld: Any = msgpack.decode(msg.pld) any_pld: Any = msgpack.decode(msg.pld)
message: str = ( message: str = (
f'invalid `{msg_type.__qualname__}` payload\n\n' f'invalid `{msg_type.__qualname__}` msg payload\n\n'
f'value: `{any_pld!r}` does not match type-spec: ' #\n' f'value: `{any_pld!r}` does not match type-spec: '
f'`{type(msg).__qualname__}.pld: {codec.pld_spec_str}`' f'`{type(msg).__qualname__}.pld: {codec.pld_spec_str}`'
# f'<{type(msg).__qualname__}(\n'
# f' |_pld: {codec.pld_spec_str}\n'# != {any_pld!r}\n'
# f')>\n\n'
) )
# src_err_msg = msg
bad_msg = msg bad_msg = msg
# TODO: should we just decode the msg to a dict despite
# only the payload being wrong?
# -[ ] maybe the better design is to break this construct
# logic into a separate explicit helper raiser-func?
else: else:
# decode the msg-bytes using the std msgpack # decode the msg-bytes using the std msgpack
@ -1308,21 +1357,21 @@ def _mk_msg_type_err(
if verb_header: if verb_header:
message = f'{verb_header} ' + message message = f'{verb_header} ' + message
# if not isinstance(bad_msg, PayloadMsg):
# import pdbp; pdbp.set_trace()
msgtyperr = MsgTypeError.from_decode( msgtyperr = MsgTypeError.from_decode(
message=message, message=message,
bad_msg=bad_msg, bad_msg=bad_msg,
bad_msg_as_dict=msg_dict, bad_msg_as_dict=msg_dict,
# NOTE: for the send-side `.started()` pld-validate # NOTE: for pld-spec MTEs we set the `._ipc_msg` manually:
# case we actually set the `._ipc_msg` AFTER we return # - for the send-side `.started()` pld-validate
# from here inside `Context.started()` since we actually # case we actually raise inline so we don't need to
# want to emulate the `Error` from the mte we build here # set the it at all.
# Bo # - for recv side we set it inside `PldRx.decode_pld()`
# so by default in that case this is set to `None` # after a manual call to `pack_error()` since we
# ipc_msg=src_err_msg, # actually want to emulate the `Error` from the mte we
# build here. So by default in that case, this is left
# as `None` here.
# ipc_msg=src_err_msg,
) )
msgtyperr.__cause__ = src_validation_error msgtyperr.__cause__ = src_validation_error
return msgtyperr return msgtyperr