forked from goodboy/tractor
1
0
Fork 0

Reorg frames pformatters, add `Context.repr_state`

A better spot for the pretty-formatting of frame text (and thus tracebacks)
is in the new `.devx._code` module:
- move from `._exceptions` -> `.devx._code.pformat_boxed_tb()`.
- add new `pformat_caller_frame()` factored out the use case in
  `._exceptions._mk_msg_type_err()` where we dump a stack trace
  for bad `.send()` side IPC msgs.

Add some new pretty-format methods to `Context`:
- explicitly implement `.pformat()` and allow an `extra_fields: dict`
  which can be used to inject additional fields (maybe eventually by
  default) such as is now used inside
  `._maybe_cancel_and_set_remote_error()` when reporting the internal
  `._scope` state in cancel logging.
- add a new `.repr_state -> str` which provides a single string status
  depending on the internal state of the IPC ctx in terms of the shuttle
  protocol's "phase"; use it from `.pformat()` for the `|_state:`.
- set `.started(complain_no_parity=False)` now since we presume decoding
  with `.pld: Raw` now with the new `PldRx` design.
- use new `msgops.current_pldrx()` in `mk_context()`.
runtime_to_msgspec
Tyler Goodlet 2024-04-30 12:37:17 -04:00
parent 40c972f0ec
commit 88a0e90f82
3 changed files with 227 additions and 121 deletions

View File

@ -61,7 +61,6 @@ from ._exceptions import (
) )
from .log import get_logger from .log import get_logger
from .msg import ( from .msg import (
_codec,
Error, Error,
MsgType, MsgType,
MsgCodec, MsgCodec,
@ -103,7 +102,6 @@ class Unresolved:
a final return value or raised error is resolved. a final return value or raised error is resolved.
''' '''
...
# TODO: make this a .msg.types.Struct! # TODO: make this a .msg.types.Struct!
@ -116,19 +114,19 @@ class Context:
NB: This class should **never be instatiated directly**, it is allocated NB: This class should **never be instatiated directly**, it is allocated
by the runtime in 2 ways: by the runtime in 2 ways:
- by entering ``Portal.open_context()`` which is the primary - by entering `Portal.open_context()` which is the primary
public API for any "caller" task or, public API for any "parent" task or,
- by the RPC machinery's `._rpc._invoke()` as a `ctx` arg - by the RPC machinery's `._rpc._invoke()` as a `ctx` arg
to a remotely scheduled "callee" function. to a remotely scheduled "child" function.
AND is always constructed using the below ``mk_context()``. AND is always constructed using the below `mk_context()`.
Allows maintaining task or protocol specific state between Allows maintaining task or protocol specific state between
2 cancel-scope-linked, communicating and parallel executing 2 cancel-scope-linked, communicating and parallel executing
`trio.Task`s. Contexts are allocated on each side of any task `trio.Task`s. Contexts are allocated on each side of any task
RPC-linked msg dialog, i.e. for every request to a remote RPC-linked msg dialog, i.e. for every request to a remote
actor from a `Portal`. On the "callee" side a context is actor from a `Portal`. On the "callee" side a context is
always allocated inside ``._rpc._invoke()``. always allocated inside `._rpc._invoke()`.
TODO: more detailed writeup on cancellation, error and TODO: more detailed writeup on cancellation, error and
streaming semantics.. streaming semantics..
@ -262,7 +260,13 @@ class Context:
_strict_started: bool = False _strict_started: bool = False
_cancel_on_msgerr: bool = True _cancel_on_msgerr: bool = True
def __str__(self) -> str: def pformat(
self,
extra_fields: dict[str, Any]|None = None,
# ^-TODO-^ some built-in extra state fields
# we'll want in some devx specific cases?
) -> str:
ds: str = '=' ds: str = '='
# ds: str = ': ' # ds: str = ': '
@ -279,11 +283,7 @@ class Context:
outcome_str: str = self.repr_outcome( outcome_str: str = self.repr_outcome(
show_error_fields=True show_error_fields=True
) )
outcome_typ_str: str = self.repr_outcome( fmtstr: str = (
type_only=True
)
return (
f'<Context(\n' f'<Context(\n'
# f'\n' # f'\n'
# f' ---\n' # f' ---\n'
@ -304,12 +304,12 @@ class Context:
# f' -----\n' # f' -----\n'
# #
# TODO: better state `str`ids? # TODO: better state `str`ids?
# -[ ] maybe map err-types to strs like 'cancelled', # -[x] maybe map err-types to strs like 'cancelled',
# 'errored', 'streaming', 'started', .. etc. # 'errored', 'streaming', 'started', .. etc.
# -[ ] as well as a final result wrapper like # -[ ] as well as a final result wrapper like
# `outcome.Value`? # `outcome.Value`?
# #
f' |_state: {outcome_typ_str}\n' f' |_state: {self.repr_state!r}\n'
f' outcome{ds}{outcome_str}\n' f' outcome{ds}{outcome_str}\n'
f' result{ds}{self._result}\n' f' result{ds}{self._result}\n'
@ -324,6 +324,16 @@ class Context:
# -[ ] remove this ^ right? # -[ ] remove this ^ right?
# f' _remote_error={self._remote_error} # f' _remote_error={self._remote_error}
)
if extra_fields:
for key, val in extra_fields.items():
fmtstr += (
f' {key}{ds}{val!r}\n'
)
return (
fmtstr
+
')>\n' ')>\n'
) )
# NOTE: making this return a value that can be passed to # NOTE: making this return a value that can be passed to
@ -335,7 +345,8 @@ class Context:
# logging perspective over `eval()`-ability since we do NOT # logging perspective over `eval()`-ability since we do NOT
# target serializing non-struct instances! # target serializing non-struct instances!
# def __repr__(self) -> str: # def __repr__(self) -> str:
__repr__ = __str__ __str__ = pformat
__repr__ = pformat
@property @property
def cancel_called(self) -> bool: def cancel_called(self) -> bool:
@ -615,10 +626,10 @@ class Context:
whom: str = ( whom: str = (
'us' if error.canceller == self._actor.uid 'us' if error.canceller == self._actor.uid
else 'peer' else 'a remote peer (not us)'
) )
log.cancel( log.cancel(
f'IPC context cancelled by {whom}!\n\n' f'IPC context was cancelled by {whom}!\n\n'
f'{error}' f'{error}'
) )
@ -626,7 +637,6 @@ class Context:
msgerr = True msgerr = True
log.error( log.error(
f'IPC dialog error due to msg-type caused by {self.peer_side!r} side\n\n' f'IPC dialog error due to msg-type caused by {self.peer_side!r} side\n\n'
f'{error}\n' f'{error}\n'
f'{pformat(self)}\n' f'{pformat(self)}\n'
) )
@ -696,24 +706,23 @@ class Context:
else: else:
message: str = 'NOT cancelling `Context._scope` !\n\n' message: str = 'NOT cancelling `Context._scope` !\n\n'
scope_info: str = 'No `self._scope: CancelScope` was set/used ?' fmt_str: str = 'No `self._scope: CancelScope` was set/used ?'
if cs: if cs:
scope_info: str = ( fmt_str: str = self.pformat(
f'self._scope: {cs}\n' extra_fields={
f'|_ .cancel_called: {cs.cancel_called}\n' '._is_self_cancelled()': self._is_self_cancelled(),
f'|_ .cancelled_caught: {cs.cancelled_caught}\n' '._cancel_on_msgerr': self._cancel_on_msgerr,
f'|_ ._cancel_status: {cs._cancel_status}\n\n'
f'{self}\n' '._scope': cs,
f'|_ ._is_self_cancelled(): {self._is_self_cancelled()}\n' '._scope.cancel_called': cs.cancel_called,
f'|_ ._cancel_on_msgerr: {self._cancel_on_msgerr}\n\n' '._scope.cancelled_caught': cs.cancelled_caught,
'._scope._cancel_status': cs._cancel_status,
f'msgerr: {msgerr}\n' }
) )
log.cancel( log.cancel(
message message
+ +
f'{scope_info}' fmt_str
) )
# TODO: maybe we should also call `._res_scope.cancel()` if it # TODO: maybe we should also call `._res_scope.cancel()` if it
# exists to support cancelling any drain loop hangs? # exists to support cancelling any drain loop hangs?
@ -748,7 +757,7 @@ class Context:
) )
return ( return (
# f'{self._nsf}() -{{{codec}}}-> {repr(self.outcome)}:' # f'{self._nsf}() -{{{codec}}}-> {repr(self.outcome)}:'
f'{self._nsf}() -> {outcome_str}:' f'{self._nsf}() -> {outcome_str}'
) )
@property @property
@ -836,7 +845,7 @@ class Context:
if not self._portal: if not self._portal:
raise InternalError( raise InternalError(
'No portal found!?\n' 'No portal found!?\n'
'Why is this supposed caller context missing it?' 'Why is this supposed {self.side!r}-side ctx task missing it?!?'
) )
cid: str = self.cid cid: str = self.cid
@ -1274,11 +1283,11 @@ class Context:
) )
log.cancel( log.cancel(
'Ctx drained pre-result msgs:\n' 'Ctx drained to final result msgs\n'
f'{pformat(drained_msgs)}\n\n' f'{return_msg}\n\n'
f'Final return msg:\n' f'pre-result drained msgs:\n'
f'{return_msg}\n' f'{pformat(drained_msgs)}\n'
) )
self.maybe_raise( self.maybe_raise(
@ -1443,6 +1452,65 @@ class Context:
repr(self._result) repr(self._result)
) )
@property
def repr_state(self) -> str:
'''
A `str`-status describing the current state of this
inter-actor IPC context in terms of the current "phase" state
of the SC shuttling dialog protocol.
'''
merr: Exception|None = self.maybe_error
outcome: Unresolved|Exception|Any = self.outcome
match (
outcome,
merr,
):
case (
Unresolved,
ContextCancelled(),
) if self.cancel_acked:
status = 'self-cancelled'
case (
Unresolved,
ContextCancelled(),
) if (
self.canceller
and not self._cancel_called
):
status = 'peer-cancelled'
case (
Unresolved,
BaseException(),
) if self.canceller:
status = 'errored'
case (
_, # any non-unresolved value
None,
) if self._final_result_is_set():
status = 'returned'
case (
Unresolved, # noqa (weird.. ruff)
None,
):
if stream := self._stream:
if stream.closed:
status = 'streaming-finished'
else:
status = 'streaming'
elif self._started_called:
status = 'started'
case _:
status = 'unknown!?'
return status
async def started( async def started(
self, self,
@ -1451,7 +1519,11 @@ class Context:
value: PayloadT|None = None, value: PayloadT|None = None,
strict_parity: bool = False, strict_parity: bool = False,
complain_no_parity: bool = True,
# TODO: this will always emit now that we do `.pld: Raw`
# passthrough.. so maybe just only complain when above strict
# flag is set?
complain_no_parity: bool = False,
) -> None: ) -> None:
''' '''
@ -1511,18 +1583,19 @@ class Context:
) )
raise RuntimeError( raise RuntimeError(
'Failed to roundtrip `Started` msg?\n' 'Failed to roundtrip `Started` msg?\n'
f'{pformat(rt_started)}\n' f'{pretty_struct.pformat(rt_started)}\n'
) )
if rt_started != started_msg: if rt_started != started_msg:
# TODO: break these methods out from the struct subtype? # TODO: break these methods out from the struct subtype?
# TODO: make that one a mod func too..
diff = pretty_struct.Struct.__sub__( diff = pretty_struct.Struct.__sub__(
rt_started, rt_started,
started_msg, started_msg,
) )
complaint: str = ( complaint: str = (
'Started value does not match after codec rountrip?\n\n' 'Started value does not match after roundtrip?\n\n'
f'{diff}' f'{diff}'
) )
@ -1538,8 +1611,6 @@ class Context:
else: else:
log.warning(complaint) log.warning(complaint)
# started_msg = rt_started
await self.chan.send(started_msg) await self.chan.send(started_msg)
# raise any msg type error NO MATTER WHAT! # raise any msg type error NO MATTER WHAT!
@ -2354,7 +2425,7 @@ async def open_context_from_portal(
# FINALLY, remove the context from runtime tracking and # FINALLY, remove the context from runtime tracking and
# exit! # exit!
log.runtime( log.runtime(
'De-allocating IPC ctx opened with {ctx.side!r} peer \n' f'De-allocating IPC ctx opened with {ctx.side!r} peer \n'
f'uid: {uid}\n' f'uid: {uid}\n'
f'cid: {ctx.cid}\n' f'cid: {ctx.cid}\n'
) )
@ -2390,10 +2461,8 @@ def mk_context(
from .devx._code import find_caller_info from .devx._code import find_caller_info
caller_info: CallerInfo|None = find_caller_info() caller_info: CallerInfo|None = find_caller_info()
pld_rx = msgops.PldRx( # TODO: when/how do we apply `.limit_plds()` from here?
# _rx_mc=recv_chan, pld_rx: msgops.PldRx = msgops.current_pldrx()
_msgdec=_codec.mk_dec(spec=pld_spec)
)
ctx = Context( ctx = Context(
chan=chan, chan=chan,

View File

@ -46,7 +46,7 @@ from tractor.msg import (
Error, Error,
MsgType, MsgType,
Stop, Stop,
Yield, # Yield,
types as msgtypes, types as msgtypes,
MsgCodec, MsgCodec,
MsgDec, MsgDec,
@ -140,71 +140,6 @@ def get_err_type(type_name: str) -> BaseException|None:
return type_ref return type_ref
def pformat_boxed_tb(
tb_str: str,
fields_str: str|None = None,
field_prefix: str = ' |_',
tb_box_indent: int|None = None,
tb_body_indent: int = 1,
) -> str:
if (
fields_str
and
field_prefix
):
fields: str = textwrap.indent(
fields_str,
prefix=field_prefix,
)
else:
fields = fields_str or ''
tb_body = tb_str
if tb_body_indent:
tb_body: str = textwrap.indent(
tb_str,
prefix=tb_body_indent * ' ',
)
tb_box: str = (
# orig
# f' |\n'
# f' ------ - ------\n\n'
# f'{tb_str}\n'
# f' ------ - ------\n'
# f' _|\n'
f'|\n'
f' ------ - ------\n\n'
# f'{tb_str}\n'
f'{tb_body}'
f' ------ - ------\n'
f'_|\n'
)
tb_box_indent: str = (
tb_box_indent
or
1
# (len(field_prefix))
# ? ^-TODO-^ ? if you wanted another indent level
)
if tb_box_indent > 0:
tb_box: str = textwrap.indent(
tb_box,
prefix=tb_box_indent * ' ',
)
return (
fields
+
tb_box
)
def pack_from_raise( def pack_from_raise(
local_err: ( local_err: (
ContextCancelled ContextCancelled
@ -504,12 +439,15 @@ class RemoteActorError(Exception):
reprol_str: str = ( reprol_str: str = (
f'{type(self).__name__}' # type name f'{type(self).__name__}' # type name
f'[{self.boxed_type_str}]' # parameterized by boxed type f'[{self.boxed_type_str}]' # parameterized by boxed type
'(' # init-style look
) )
_repr: str = self._mk_fields_str( _repr: str = self._mk_fields_str(
self.reprol_fields, self.reprol_fields,
end_char=' ', end_char=' ',
) )
if _repr:
reprol_str += '(' # init-style call
return ( return (
reprol_str reprol_str
+ +
@ -521,6 +459,7 @@ class RemoteActorError(Exception):
Nicely formatted boxed error meta data + traceback. Nicely formatted boxed error meta data + traceback.
''' '''
from tractor.devx._code import pformat_boxed_tb
fields: str = self._mk_fields_str( fields: str = self._mk_fields_str(
_body_fields _body_fields
+ +
@ -1092,14 +1031,10 @@ def _mk_msg_type_err(
# no src error from `msgspec.msgpack.Decoder.decode()` so # no src error from `msgspec.msgpack.Decoder.decode()` so
# prolly a manual type-check on our part. # prolly a manual type-check on our part.
if message is None: if message is None:
fmt_stack: str = ( from tractor.devx._code import (
'\n'.join(traceback.format_stack(limit=3)) pformat_caller_frame,
)
tb_fmt: str = pformat_boxed_tb(
tb_str=fmt_stack,
field_prefix=' ',
indent='',
) )
tb_fmt: str = pformat_caller_frame(stack_limit=3)
message: str = ( message: str = (
f'invalid msg -> {msg}: {type(msg)}\n\n' f'invalid msg -> {msg}: {type(msg)}\n\n'
f'{tb_fmt}\n' f'{tb_fmt}\n'

View File

@ -23,6 +23,8 @@ from __future__ import annotations
import inspect import inspect
# import msgspec # import msgspec
# from pprint import pformat # from pprint import pformat
import textwrap
import traceback
from types import ( from types import (
FrameType, FrameType,
FunctionType, FunctionType,
@ -175,3 +177,103 @@ def find_caller_info(
) )
return None return None
def pformat_boxed_tb(
tb_str: str,
fields_str: str|None = None,
field_prefix: str = ' |_',
tb_box_indent: int|None = None,
tb_body_indent: int = 1,
) -> str:
'''
Create a "boxed" looking traceback string.
Useful for emphasizing traceback text content as being an
embedded attribute of some other object (like
a `RemoteActorError` or other boxing remote error shuttle
container).
Any other parent/container "fields" can be passed in the
`fields_str` input along with other prefix/indent settings.
'''
if (
fields_str
and
field_prefix
):
fields: str = textwrap.indent(
fields_str,
prefix=field_prefix,
)
else:
fields = fields_str or ''
tb_body = tb_str
if tb_body_indent:
tb_body: str = textwrap.indent(
tb_str,
prefix=tb_body_indent * ' ',
)
tb_box: str = (
# orig
# f' |\n'
# f' ------ - ------\n\n'
# f'{tb_str}\n'
# f' ------ - ------\n'
# f' _|\n'
f'|\n'
f' ------ - ------\n\n'
# f'{tb_str}\n'
f'{tb_body}'
f' ------ - ------\n'
f'_|\n'
)
tb_box_indent: str = (
tb_box_indent
or
1
# (len(field_prefix))
# ? ^-TODO-^ ? if you wanted another indent level
)
if tb_box_indent > 0:
tb_box: str = textwrap.indent(
tb_box,
prefix=tb_box_indent * ' ',
)
return (
fields
+
tb_box
)
def pformat_caller_frame(
stack_limit: int = 1,
box_tb: bool = True,
) -> str:
'''
Capture and return the traceback text content from
`stack_limit` call frames up.
'''
tb_str: str = (
'\n'.join(
traceback.format_stack(limit=stack_limit)
)
)
if box_tb:
tb_str: str = pformat_boxed_tb(
tb_str=tb_str,
field_prefix=' ',
indent='',
)
return tb_str