Allocate a `PldRx` per `Context`, new pld-spec API
Since the state mgmt becomes quite messy with multiple sub-tasks inside an IPC ctx, AND bc generally speaking the payload-type-spec should map 1-to-1 with the `Context`, it doesn't make a lot of sense to be using `ContextVar`s to modify the `Context.pld_rx: PldRx` instance. Instead, always allocate a full instance inside `mk_context()` with the default `.pld_rx: PldRx` set to use the `msg._ops._def_any_pldec: MsgDec` In support, simplify the `.msg._ops` impl and APIs: - drop `_ctxvar_PldRx`, `_def_pld_rx` and `current_pldrx()`. - rename `PldRx._pldec` -> `._pld_dec`. - rename the unused `PldRx.apply_to_ipc()` -> `.wraps_ipc()`. - add a required `PldRx._ctx: Context` attr since it is needed internally in some meths and each pld-rx now maps to a specific ctx. - modify all recv methods to accept a `ipc: Context|MsgStream` (instead of a `ctx` arg) since both have a ref to the same `._rx_chan` and there are only a couple spots (in `.dec_msg()`) where we need the `ctx` explicitly (which can now be easily accessed via a new `MsgStream.ctx` property, see below). - always show the `.dec_msg()` frame in tbs if there's a reference error when calling `_raise_from_unexpected_msg()` in the fallthrough case. - implement `limit_plds()` as light wrapper around getting the `current_ipc_ctx()` and mutating its `MsgDec` via `Context.pld_rx.limit_plds()`. - add a `maybe_limit_plds()` which just provides an `@acm` equivalent of `limit_plds()` handy for composing in a `async with ():` style block (avoiding additional indent levels in the body of async funcs). Obvi extend the `Context` and `MsgStream` interfaces as needed to match the above: - add a `Context.pld_rx` pub prop. - new private refs to `Context._started_msg: Started` and a `._started_pld` (mostly for internal debugging / testing / logging) and set inside `.open_context()` immediately after the syncing phase. - a `Context.has_outcome() -> bool:` predicate which can be used to more easily determine if the ctx errored or has a final result. - pub props for `MsgStream.ctx: Context` and `.chan: Channel` providing full `ipc`-arg compat with the `PldRx` method signatures.runtime_to_msgspec
parent
d93135acd8
commit
262a0e36c6
|
@ -41,6 +41,7 @@ from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
Mapping,
|
Mapping,
|
||||||
Type,
|
Type,
|
||||||
|
TypeAlias,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
@ -155,6 +156,41 @@ class Context:
|
||||||
# payload receiver
|
# payload receiver
|
||||||
_pld_rx: msgops.PldRx
|
_pld_rx: msgops.PldRx
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pld_rx(self) -> msgops.PldRx:
|
||||||
|
'''
|
||||||
|
The current `tractor.Context`'s msg-payload-receiver.
|
||||||
|
|
||||||
|
A payload receiver is the IPC-msg processing sub-sys which
|
||||||
|
filters inter-actor-task communicated payload data, i.e. the
|
||||||
|
`PayloadMsg.pld: PayloadT` field value, AFTER its container
|
||||||
|
shuttlle msg (eg. `Started`/`Yield`/`Return) has been
|
||||||
|
delivered up from `tractor`'s transport layer but BEFORE the
|
||||||
|
data is yielded to `tractor` application code.
|
||||||
|
|
||||||
|
The "IPC-primitive API" is normally one of a `Context` (this)` or a `MsgStream`
|
||||||
|
or some higher level API using one of them.
|
||||||
|
|
||||||
|
For ex. `pld_data: PayloadT = MsgStream.receive()` implicitly
|
||||||
|
calls into the stream's parent `Context.pld_rx.recv_pld().` to
|
||||||
|
receive the latest `PayloadMsg.pld` value.
|
||||||
|
|
||||||
|
Modification of the current payload spec via `limit_plds()`
|
||||||
|
allows a `tractor` application to contextually filter IPC
|
||||||
|
payload content with a type specification as supported by the
|
||||||
|
interchange backend.
|
||||||
|
|
||||||
|
- for `msgspec` see <PUTLINKHERE>.
|
||||||
|
|
||||||
|
Note that the `PldRx` itself is a per-`Context` instance that
|
||||||
|
normally only changes when some (sub-)task, on a given "side"
|
||||||
|
of the IPC ctx (either a "child"-side RPC or inside
|
||||||
|
a "parent"-side `Portal.open_context()` block), modifies it
|
||||||
|
using the `.msg._ops.limit_plds()` API.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return self._pld_rx
|
||||||
|
|
||||||
# full "namespace-path" to target RPC function
|
# full "namespace-path" to target RPC function
|
||||||
_nsf: NamespacePath
|
_nsf: NamespacePath
|
||||||
|
|
||||||
|
@ -231,6 +267,8 @@ class Context:
|
||||||
|
|
||||||
# init and streaming state
|
# init and streaming state
|
||||||
_started_called: bool = False
|
_started_called: bool = False
|
||||||
|
_started_msg: MsgType|None = None
|
||||||
|
_started_pld: Any = None
|
||||||
_stream_opened: bool = False
|
_stream_opened: bool = False
|
||||||
_stream: MsgStream|None = None
|
_stream: MsgStream|None = None
|
||||||
|
|
||||||
|
@ -623,7 +661,7 @@ class Context:
|
||||||
log.runtime(
|
log.runtime(
|
||||||
'Setting remote error for ctx\n\n'
|
'Setting remote error for ctx\n\n'
|
||||||
f'<= {self.peer_side!r}: {self.chan.uid}\n'
|
f'<= {self.peer_side!r}: {self.chan.uid}\n'
|
||||||
f'=> {self.side!r}\n\n'
|
f'=> {self.side!r}: {self._actor.uid}\n\n'
|
||||||
f'{error}'
|
f'{error}'
|
||||||
)
|
)
|
||||||
self._remote_error: BaseException = error
|
self._remote_error: BaseException = error
|
||||||
|
@ -678,7 +716,7 @@ class Context:
|
||||||
log.error(
|
log.error(
|
||||||
f'Remote context error:\n\n'
|
f'Remote context error:\n\n'
|
||||||
# f'{pformat(self)}\n'
|
# f'{pformat(self)}\n'
|
||||||
f'{error}\n'
|
f'{error}'
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._canceller is None:
|
if self._canceller is None:
|
||||||
|
@ -724,8 +762,10 @@ class Context:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
message: str = 'NOT cancelling `Context._scope` !\n\n'
|
message: str = 'NOT cancelling `Context._scope` !\n\n'
|
||||||
|
# from .devx import mk_pdb
|
||||||
|
# mk_pdb().set_trace()
|
||||||
|
|
||||||
fmt_str: str = 'No `self._scope: CancelScope` was set/used ?'
|
fmt_str: str = 'No `self._scope: CancelScope` was set/used ?\n'
|
||||||
if (
|
if (
|
||||||
cs
|
cs
|
||||||
and
|
and
|
||||||
|
@ -805,6 +845,7 @@ class Context:
|
||||||
# f'{ci.api_nsp}()\n'
|
# f'{ci.api_nsp}()\n'
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
# TODO: use `.dev._frame_stack` scanning to find caller!
|
||||||
return 'Portal.open_context()'
|
return 'Portal.open_context()'
|
||||||
|
|
||||||
async def cancel(
|
async def cancel(
|
||||||
|
@ -1304,17 +1345,6 @@ class Context:
|
||||||
ctx=self,
|
ctx=self,
|
||||||
hide_tb=hide_tb,
|
hide_tb=hide_tb,
|
||||||
)
|
)
|
||||||
for msg in drained_msgs:
|
|
||||||
|
|
||||||
# TODO: mask this by default..
|
|
||||||
if isinstance(msg, Return):
|
|
||||||
# from .devx import pause
|
|
||||||
# await pause()
|
|
||||||
# raise InternalError(
|
|
||||||
log.warning(
|
|
||||||
'Final `return` msg should never be drained !?!?\n\n'
|
|
||||||
f'{msg}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
drained_status: str = (
|
drained_status: str = (
|
||||||
'Ctx drained to final outcome msg\n\n'
|
'Ctx drained to final outcome msg\n\n'
|
||||||
|
@ -1435,6 +1465,10 @@ class Context:
|
||||||
self._result
|
self._result
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_outcome(self) -> bool:
|
||||||
|
return bool(self.maybe_error) or self._final_result_is_set()
|
||||||
|
|
||||||
# @property
|
# @property
|
||||||
def repr_outcome(
|
def repr_outcome(
|
||||||
self,
|
self,
|
||||||
|
@ -1637,8 +1671,6 @@ class Context:
|
||||||
)
|
)
|
||||||
|
|
||||||
if rt_started != started_msg:
|
if rt_started != started_msg:
|
||||||
# TODO: break these methods out from the struct subtype?
|
|
||||||
|
|
||||||
# TODO: make that one a mod func too..
|
# TODO: make that one a mod func too..
|
||||||
diff = pretty_struct.Struct.__sub__(
|
diff = pretty_struct.Struct.__sub__(
|
||||||
rt_started,
|
rt_started,
|
||||||
|
@ -1674,6 +1706,8 @@ class Context:
|
||||||
) from verr
|
) from verr
|
||||||
|
|
||||||
self._started_called = True
|
self._started_called = True
|
||||||
|
self._started_msg = started_msg
|
||||||
|
self._started_pld = value
|
||||||
|
|
||||||
async def _drain_overflows(
|
async def _drain_overflows(
|
||||||
self,
|
self,
|
||||||
|
@ -1961,6 +1995,7 @@ async def open_context_from_portal(
|
||||||
portal: Portal,
|
portal: Portal,
|
||||||
func: Callable,
|
func: Callable,
|
||||||
|
|
||||||
|
pld_spec: TypeAlias|None = None,
|
||||||
allow_overruns: bool = False,
|
allow_overruns: bool = False,
|
||||||
|
|
||||||
# TODO: if we set this the wrapping `@acm` body will
|
# TODO: if we set this the wrapping `@acm` body will
|
||||||
|
@ -2026,7 +2061,7 @@ async def open_context_from_portal(
|
||||||
# XXX NOTE XXX: currenly we do NOT allow opening a contex
|
# XXX NOTE XXX: currenly we do NOT allow opening a contex
|
||||||
# with "self" since the local feeder mem-chan processing
|
# with "self" since the local feeder mem-chan processing
|
||||||
# is not built for it.
|
# is not built for it.
|
||||||
if portal.channel.uid == portal.actor.uid:
|
if (uid := portal.channel.uid) == portal.actor.uid:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'** !! Invalid Operation !! **\n'
|
'** !! Invalid Operation !! **\n'
|
||||||
'Can not open an IPC ctx with the local actor!\n'
|
'Can not open an IPC ctx with the local actor!\n'
|
||||||
|
@ -2054,6 +2089,21 @@ async def open_context_from_portal(
|
||||||
assert ctx._caller_info
|
assert ctx._caller_info
|
||||||
_ctxvar_Context.set(ctx)
|
_ctxvar_Context.set(ctx)
|
||||||
|
|
||||||
|
# placeholder for any exception raised in the runtime
|
||||||
|
# or by user tasks which cause this context's closure.
|
||||||
|
scope_err: BaseException|None = None
|
||||||
|
ctxc_from_callee: ContextCancelled|None = None
|
||||||
|
try:
|
||||||
|
async with (
|
||||||
|
trio.open_nursery() as tn,
|
||||||
|
msgops.maybe_limit_plds(
|
||||||
|
ctx=ctx,
|
||||||
|
spec=pld_spec,
|
||||||
|
) as maybe_msgdec,
|
||||||
|
):
|
||||||
|
if maybe_msgdec:
|
||||||
|
assert maybe_msgdec.pld_spec == pld_spec
|
||||||
|
|
||||||
# XXX NOTE since `._scope` is NOT set BEFORE we retreive the
|
# XXX NOTE since `._scope` is NOT set BEFORE we retreive the
|
||||||
# `Started`-msg any cancellation triggered
|
# `Started`-msg any cancellation triggered
|
||||||
# in `._maybe_cancel_and_set_remote_error()` will
|
# in `._maybe_cancel_and_set_remote_error()` will
|
||||||
|
@ -2061,25 +2111,23 @@ async def open_context_from_portal(
|
||||||
# -> it's expected that if there is an error in this phase of
|
# -> it's expected that if there is an error in this phase of
|
||||||
# the dialog, the `Error` msg should be raised from the `msg`
|
# the dialog, the `Error` msg should be raised from the `msg`
|
||||||
# handling block below.
|
# handling block below.
|
||||||
first: Any = await ctx._pld_rx.recv_pld(
|
started_msg, first = await ctx._pld_rx.recv_msg_w_pld(
|
||||||
ctx=ctx,
|
ipc=ctx,
|
||||||
expect_msg=Started,
|
expect_msg=Started,
|
||||||
|
passthrough_non_pld_msgs=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# from .devx import pause
|
||||||
|
# await pause()
|
||||||
ctx._started_called: bool = True
|
ctx._started_called: bool = True
|
||||||
|
ctx._started_msg: bool = started_msg
|
||||||
|
ctx._started_pld: bool = first
|
||||||
|
|
||||||
uid: tuple = portal.channel.uid
|
# NOTE: this in an implicit runtime nursery used to,
|
||||||
cid: str = ctx.cid
|
# - start overrun queuing tasks when as well as
|
||||||
|
# for cancellation of the scope opened by the user.
|
||||||
# placeholder for any exception raised in the runtime
|
ctx._scope_nursery: trio.Nursery = tn
|
||||||
# or by user tasks which cause this context's closure.
|
ctx._scope: trio.CancelScope = tn.cancel_scope
|
||||||
scope_err: BaseException|None = None
|
|
||||||
ctxc_from_callee: ContextCancelled|None = None
|
|
||||||
try:
|
|
||||||
async with trio.open_nursery() as nurse:
|
|
||||||
|
|
||||||
# NOTE: used to start overrun queuing tasks
|
|
||||||
ctx._scope_nursery: trio.Nursery = nurse
|
|
||||||
ctx._scope: trio.CancelScope = nurse.cancel_scope
|
|
||||||
|
|
||||||
# deliver context instance and .started() msg value
|
# deliver context instance and .started() msg value
|
||||||
# in enter tuple.
|
# in enter tuple.
|
||||||
|
@ -2126,13 +2174,13 @@ async def open_context_from_portal(
|
||||||
|
|
||||||
# when in allow_overruns mode there may be
|
# when in allow_overruns mode there may be
|
||||||
# lingering overflow sender tasks remaining?
|
# lingering overflow sender tasks remaining?
|
||||||
if nurse.child_tasks:
|
if tn.child_tasks:
|
||||||
# XXX: ensure we are in overrun state
|
# XXX: ensure we are in overrun state
|
||||||
# with ``._allow_overruns=True`` bc otherwise
|
# with ``._allow_overruns=True`` bc otherwise
|
||||||
# there should be no tasks in this nursery!
|
# there should be no tasks in this nursery!
|
||||||
if (
|
if (
|
||||||
not ctx._allow_overruns
|
not ctx._allow_overruns
|
||||||
or len(nurse.child_tasks) > 1
|
or len(tn.child_tasks) > 1
|
||||||
):
|
):
|
||||||
raise InternalError(
|
raise InternalError(
|
||||||
'Context has sub-tasks but is '
|
'Context has sub-tasks but is '
|
||||||
|
@ -2304,7 +2352,7 @@ async def open_context_from_portal(
|
||||||
):
|
):
|
||||||
log.warning(
|
log.warning(
|
||||||
'IPC connection for context is broken?\n'
|
'IPC connection for context is broken?\n'
|
||||||
f'task:{cid}\n'
|
f'task: {ctx.cid}\n'
|
||||||
f'actor: {uid}'
|
f'actor: {uid}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2455,9 +2503,8 @@ async def open_context_from_portal(
|
||||||
and ctx.cancel_acked
|
and ctx.cancel_acked
|
||||||
):
|
):
|
||||||
log.cancel(
|
log.cancel(
|
||||||
'Context cancelled by {ctx.side!r}-side task\n'
|
f'Context cancelled by {ctx.side!r}-side task\n'
|
||||||
f'|_{ctx._task}\n\n'
|
f'|_{ctx._task}\n\n'
|
||||||
|
|
||||||
f'{repr(scope_err)}\n'
|
f'{repr(scope_err)}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2485,7 +2532,7 @@ async def open_context_from_portal(
|
||||||
f'cid: {ctx.cid}\n'
|
f'cid: {ctx.cid}\n'
|
||||||
)
|
)
|
||||||
portal.actor._contexts.pop(
|
portal.actor._contexts.pop(
|
||||||
(uid, cid),
|
(uid, ctx.cid),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2516,8 +2563,9 @@ def mk_context(
|
||||||
from .devx._frame_stack import find_caller_info
|
from .devx._frame_stack import find_caller_info
|
||||||
caller_info: CallerInfo|None = find_caller_info()
|
caller_info: CallerInfo|None = find_caller_info()
|
||||||
|
|
||||||
# TODO: when/how do we apply `.limit_plds()` from here?
|
pld_rx = msgops.PldRx(
|
||||||
pld_rx: msgops.PldRx = msgops.current_pldrx()
|
_pld_dec=msgops._def_any_pldec,
|
||||||
|
)
|
||||||
|
|
||||||
ctx = Context(
|
ctx = Context(
|
||||||
chan=chan,
|
chan=chan,
|
||||||
|
@ -2531,13 +2579,16 @@ def mk_context(
|
||||||
_caller_info=caller_info,
|
_caller_info=caller_info,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
pld_rx._ctx = ctx
|
||||||
ctx._result = Unresolved
|
ctx._result = Unresolved
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
# TODO: use the new type-parameters to annotate this in 3.13?
|
# TODO: use the new type-parameters to annotate this in 3.13?
|
||||||
# -[ ] https://peps.python.org/pep-0718/#unknown-types
|
# -[ ] https://peps.python.org/pep-0718/#unknown-types
|
||||||
def context(func: Callable) -> Callable:
|
def context(
|
||||||
|
func: Callable,
|
||||||
|
) -> Callable:
|
||||||
'''
|
'''
|
||||||
Mark an (async) function as an SC-supervised, inter-`Actor`,
|
Mark an (async) function as an SC-supervised, inter-`Actor`,
|
||||||
child-`trio.Task`, IPC endpoint otherwise known more
|
child-`trio.Task`, IPC endpoint otherwise known more
|
||||||
|
|
|
@ -52,6 +52,7 @@ from tractor.msg import (
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._context import Context
|
from ._context import Context
|
||||||
|
from ._ipc import Channel
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
@ -65,10 +66,10 @@ log = get_logger(__name__)
|
||||||
class MsgStream(trio.abc.Channel):
|
class MsgStream(trio.abc.Channel):
|
||||||
'''
|
'''
|
||||||
A bidirectional message stream for receiving logically sequenced
|
A bidirectional message stream for receiving logically sequenced
|
||||||
values over an inter-actor IPC ``Channel``.
|
values over an inter-actor IPC `Channel`.
|
||||||
|
|
||||||
This is the type returned to a local task which entered either
|
This is the type returned to a local task which entered either
|
||||||
``Portal.open_stream_from()`` or ``Context.open_stream()``.
|
`Portal.open_stream_from()` or `Context.open_stream()`.
|
||||||
|
|
||||||
Termination rules:
|
Termination rules:
|
||||||
|
|
||||||
|
@ -95,6 +96,22 @@ class MsgStream(trio.abc.Channel):
|
||||||
self._eoc: bool|trio.EndOfChannel = False
|
self._eoc: bool|trio.EndOfChannel = False
|
||||||
self._closed: bool|trio.ClosedResourceError = False
|
self._closed: bool|trio.ClosedResourceError = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ctx(self) -> Context:
|
||||||
|
'''
|
||||||
|
This stream's IPC `Context` ref.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return self._ctx
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chan(self) -> Channel:
|
||||||
|
'''
|
||||||
|
Ref to the containing `Context`'s transport `Channel`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return self._ctx.chan
|
||||||
|
|
||||||
# TODO: could we make this a direct method bind to `PldRx`?
|
# TODO: could we make this a direct method bind to `PldRx`?
|
||||||
# -> receive_nowait = PldRx.recv_pld
|
# -> receive_nowait = PldRx.recv_pld
|
||||||
# |_ means latter would have to accept `MsgStream`-as-`self`?
|
# |_ means latter would have to accept `MsgStream`-as-`self`?
|
||||||
|
@ -109,7 +126,7 @@ class MsgStream(trio.abc.Channel):
|
||||||
):
|
):
|
||||||
ctx: Context = self._ctx
|
ctx: Context = self._ctx
|
||||||
return ctx._pld_rx.recv_pld_nowait(
|
return ctx._pld_rx.recv_pld_nowait(
|
||||||
ctx=ctx,
|
ipc=self,
|
||||||
expect_msg=expect_msg,
|
expect_msg=expect_msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -148,7 +165,7 @@ class MsgStream(trio.abc.Channel):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
ctx: Context = self._ctx
|
ctx: Context = self._ctx
|
||||||
return await ctx._pld_rx.recv_pld(ctx=ctx)
|
return await ctx._pld_rx.recv_pld(ipc=self)
|
||||||
|
|
||||||
# XXX: the stream terminates on either of:
|
# XXX: the stream terminates on either of:
|
||||||
# - via `self._rx_chan.receive()` raising after manual closure
|
# - via `self._rx_chan.receive()` raising after manual closure
|
||||||
|
|
|
@ -22,10 +22,9 @@ operational helpers for processing transaction flows.
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
# asynccontextmanager as acm,
|
asynccontextmanager as acm,
|
||||||
contextmanager as cm,
|
contextmanager as cm,
|
||||||
)
|
)
|
||||||
from contextvars import ContextVar
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Type,
|
Type,
|
||||||
|
@ -50,6 +49,7 @@ from tractor._exceptions import (
|
||||||
_mk_msg_type_err,
|
_mk_msg_type_err,
|
||||||
pack_from_raise,
|
pack_from_raise,
|
||||||
)
|
)
|
||||||
|
from tractor._state import current_ipc_ctx
|
||||||
from ._codec import (
|
from ._codec import (
|
||||||
mk_dec,
|
mk_dec,
|
||||||
MsgDec,
|
MsgDec,
|
||||||
|
@ -75,7 +75,7 @@ if TYPE_CHECKING:
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
_def_any_pldec: MsgDec = mk_dec()
|
_def_any_pldec: MsgDec[Any] = mk_dec()
|
||||||
|
|
||||||
|
|
||||||
class PldRx(Struct):
|
class PldRx(Struct):
|
||||||
|
@ -104,15 +104,19 @@ class PldRx(Struct):
|
||||||
'''
|
'''
|
||||||
# TODO: better to bind it here?
|
# TODO: better to bind it here?
|
||||||
# _rx_mc: trio.MemoryReceiveChannel
|
# _rx_mc: trio.MemoryReceiveChannel
|
||||||
_pldec: MsgDec
|
_pld_dec: MsgDec
|
||||||
|
_ctx: Context|None = None
|
||||||
_ipc: Context|MsgStream|None = None
|
_ipc: Context|MsgStream|None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pld_dec(self) -> MsgDec:
|
def pld_dec(self) -> MsgDec:
|
||||||
return self._pldec
|
return self._pld_dec
|
||||||
|
|
||||||
|
# TODO: a better name?
|
||||||
|
# -[ ] when would this be used as it avoids needingn to pass the
|
||||||
|
# ipc prim to every method
|
||||||
@cm
|
@cm
|
||||||
def apply_to_ipc(
|
def wraps_ipc(
|
||||||
self,
|
self,
|
||||||
ipc_prim: Context|MsgStream,
|
ipc_prim: Context|MsgStream,
|
||||||
|
|
||||||
|
@ -140,49 +144,50 @@ class PldRx(Struct):
|
||||||
exit.
|
exit.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
orig_dec: MsgDec = self._pldec
|
orig_dec: MsgDec = self._pld_dec
|
||||||
limit_dec: MsgDec = mk_dec(spec=spec)
|
limit_dec: MsgDec = mk_dec(spec=spec)
|
||||||
try:
|
try:
|
||||||
self._pldec = limit_dec
|
self._pld_dec = limit_dec
|
||||||
yield limit_dec
|
yield limit_dec
|
||||||
finally:
|
finally:
|
||||||
self._pldec = orig_dec
|
self._pld_dec = orig_dec
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dec(self) -> msgpack.Decoder:
|
def dec(self) -> msgpack.Decoder:
|
||||||
return self._pldec.dec
|
return self._pld_dec.dec
|
||||||
|
|
||||||
def recv_pld_nowait(
|
def recv_pld_nowait(
|
||||||
self,
|
self,
|
||||||
# TODO: make this `MsgStream` compat as well, see above^
|
# TODO: make this `MsgStream` compat as well, see above^
|
||||||
# ipc_prim: Context|MsgStream,
|
# ipc_prim: Context|MsgStream,
|
||||||
ctx: Context,
|
ipc: Context|MsgStream,
|
||||||
|
|
||||||
ipc_msg: MsgType|None = None,
|
ipc_msg: MsgType|None = None,
|
||||||
expect_msg: Type[MsgType]|None = None,
|
expect_msg: Type[MsgType]|None = None,
|
||||||
|
hide_tb: bool = False,
|
||||||
**dec_msg_kwargs,
|
**dec_msg_kwargs,
|
||||||
|
|
||||||
) -> Any|Raw:
|
) -> Any|Raw:
|
||||||
__tracebackhide__: bool = True
|
__tracebackhide__: bool = hide_tb
|
||||||
|
|
||||||
msg: MsgType = (
|
msg: MsgType = (
|
||||||
ipc_msg
|
ipc_msg
|
||||||
or
|
or
|
||||||
|
|
||||||
# sync-rx msg from underlying IPC feeder (mem-)chan
|
# sync-rx msg from underlying IPC feeder (mem-)chan
|
||||||
ctx._rx_chan.receive_nowait()
|
ipc._rx_chan.receive_nowait()
|
||||||
)
|
)
|
||||||
return self.dec_msg(
|
return self.dec_msg(
|
||||||
msg,
|
msg,
|
||||||
ctx=ctx,
|
ipc=ipc,
|
||||||
expect_msg=expect_msg,
|
expect_msg=expect_msg,
|
||||||
|
hide_tb=hide_tb,
|
||||||
**dec_msg_kwargs,
|
**dec_msg_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def recv_pld(
|
async def recv_pld(
|
||||||
self,
|
self,
|
||||||
ctx: Context,
|
ipc: Context|MsgStream,
|
||||||
ipc_msg: MsgType|None = None,
|
ipc_msg: MsgType|None = None,
|
||||||
expect_msg: Type[MsgType]|None = None,
|
expect_msg: Type[MsgType]|None = None,
|
||||||
hide_tb: bool = True,
|
hide_tb: bool = True,
|
||||||
|
@ -200,11 +205,11 @@ class PldRx(Struct):
|
||||||
or
|
or
|
||||||
|
|
||||||
# async-rx msg from underlying IPC feeder (mem-)chan
|
# async-rx msg from underlying IPC feeder (mem-)chan
|
||||||
await ctx._rx_chan.receive()
|
await ipc._rx_chan.receive()
|
||||||
)
|
)
|
||||||
return self.dec_msg(
|
return self.dec_msg(
|
||||||
msg=msg,
|
msg=msg,
|
||||||
ctx=ctx,
|
ipc=ipc,
|
||||||
expect_msg=expect_msg,
|
expect_msg=expect_msg,
|
||||||
**dec_msg_kwargs,
|
**dec_msg_kwargs,
|
||||||
)
|
)
|
||||||
|
@ -212,7 +217,7 @@ class PldRx(Struct):
|
||||||
def dec_msg(
|
def dec_msg(
|
||||||
self,
|
self,
|
||||||
msg: MsgType,
|
msg: MsgType,
|
||||||
ctx: Context,
|
ipc: Context|MsgStream,
|
||||||
expect_msg: Type[MsgType]|None,
|
expect_msg: Type[MsgType]|None,
|
||||||
|
|
||||||
raise_error: bool = True,
|
raise_error: bool = True,
|
||||||
|
@ -225,6 +230,9 @@ class PldRx(Struct):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
|
|
||||||
|
_src_err = None
|
||||||
|
src_err: BaseException|None = None
|
||||||
match msg:
|
match msg:
|
||||||
# payload-data shuttle msg; deliver the `.pld` value
|
# payload-data shuttle msg; deliver the `.pld` value
|
||||||
# directly to IPC (primitive) client-consumer code.
|
# directly to IPC (primitive) client-consumer code.
|
||||||
|
@ -234,7 +242,7 @@ class PldRx(Struct):
|
||||||
|Return(pld=pld) # termination phase
|
|Return(pld=pld) # termination phase
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
pld: PayloadT = self._pldec.decode(pld)
|
pld: PayloadT = self._pld_dec.decode(pld)
|
||||||
log.runtime(
|
log.runtime(
|
||||||
'Decoded msg payload\n\n'
|
'Decoded msg payload\n\n'
|
||||||
f'{msg}\n\n'
|
f'{msg}\n\n'
|
||||||
|
@ -243,25 +251,30 @@ class PldRx(Struct):
|
||||||
)
|
)
|
||||||
return pld
|
return pld
|
||||||
|
|
||||||
# XXX pld-type failure
|
# XXX pld-value type failure
|
||||||
except ValidationError as src_err:
|
except ValidationError as valerr:
|
||||||
|
# pack mgterr into error-msg for
|
||||||
|
# reraise below; ensure remote-actor-err
|
||||||
|
# info is displayed nicely?
|
||||||
msgterr: MsgTypeError = _mk_msg_type_err(
|
msgterr: MsgTypeError = _mk_msg_type_err(
|
||||||
msg=msg,
|
msg=msg,
|
||||||
codec=self.pld_dec,
|
codec=self.pld_dec,
|
||||||
src_validation_error=src_err,
|
src_validation_error=valerr,
|
||||||
is_invalid_payload=True,
|
is_invalid_payload=True,
|
||||||
)
|
)
|
||||||
msg: Error = pack_from_raise(
|
msg: Error = pack_from_raise(
|
||||||
local_err=msgterr,
|
local_err=msgterr,
|
||||||
cid=msg.cid,
|
cid=msg.cid,
|
||||||
src_uid=ctx.chan.uid,
|
src_uid=ipc.chan.uid,
|
||||||
)
|
)
|
||||||
|
src_err = valerr
|
||||||
|
|
||||||
# XXX some other decoder specific failure?
|
# XXX some other decoder specific failure?
|
||||||
# except TypeError as src_error:
|
# except TypeError as src_error:
|
||||||
# from .devx import mk_pdb
|
# from .devx import mk_pdb
|
||||||
# mk_pdb().set_trace()
|
# mk_pdb().set_trace()
|
||||||
# raise src_error
|
# raise src_error
|
||||||
|
# ^-TODO-^ can remove?
|
||||||
|
|
||||||
# a runtime-internal RPC endpoint response.
|
# a runtime-internal RPC endpoint response.
|
||||||
# always passthrough since (internal) runtime
|
# always passthrough since (internal) runtime
|
||||||
|
@ -299,6 +312,7 @@ class PldRx(Struct):
|
||||||
return src_err
|
return src_err
|
||||||
|
|
||||||
case Stop(cid=cid):
|
case Stop(cid=cid):
|
||||||
|
ctx: Context = getattr(ipc, 'ctx', ipc)
|
||||||
message: str = (
|
message: str = (
|
||||||
f'{ctx.side!r}-side of ctx received stream-`Stop` from '
|
f'{ctx.side!r}-side of ctx received stream-`Stop` from '
|
||||||
f'{ctx.peer_side!r} peer ?\n'
|
f'{ctx.peer_side!r} peer ?\n'
|
||||||
|
@ -341,14 +355,21 @@ class PldRx(Struct):
|
||||||
# |_https://docs.python.org/3.11/library/exceptions.html#BaseException.add_note
|
# |_https://docs.python.org/3.11/library/exceptions.html#BaseException.add_note
|
||||||
#
|
#
|
||||||
# fallthrough and raise from `src_err`
|
# fallthrough and raise from `src_err`
|
||||||
|
try:
|
||||||
_raise_from_unexpected_msg(
|
_raise_from_unexpected_msg(
|
||||||
ctx=ctx,
|
ctx=getattr(ipc, 'ctx', ipc),
|
||||||
msg=msg,
|
msg=msg,
|
||||||
src_err=src_err,
|
src_err=src_err,
|
||||||
log=log,
|
log=log,
|
||||||
expect_msg=expect_msg,
|
expect_msg=expect_msg,
|
||||||
hide_tb=hide_tb,
|
hide_tb=hide_tb,
|
||||||
)
|
)
|
||||||
|
except UnboundLocalError:
|
||||||
|
# XXX if there's an internal lookup error in the above
|
||||||
|
# code (prolly on `src_err`) we want to show this frame
|
||||||
|
# in the tb!
|
||||||
|
__tracebackhide__: bool = False
|
||||||
|
raise
|
||||||
|
|
||||||
async def recv_msg_w_pld(
|
async def recv_msg_w_pld(
|
||||||
self,
|
self,
|
||||||
|
@ -378,52 +399,13 @@ class PldRx(Struct):
|
||||||
# msg instance?
|
# msg instance?
|
||||||
pld: PayloadT = self.dec_msg(
|
pld: PayloadT = self.dec_msg(
|
||||||
msg,
|
msg,
|
||||||
ctx=ipc,
|
ipc=ipc,
|
||||||
expect_msg=expect_msg,
|
expect_msg=expect_msg,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
return msg, pld
|
return msg, pld
|
||||||
|
|
||||||
|
|
||||||
# Always maintain a task-context-global `PldRx`
|
|
||||||
_def_pld_rx: PldRx = PldRx(
|
|
||||||
_pldec=_def_any_pldec,
|
|
||||||
)
|
|
||||||
_ctxvar_PldRx: ContextVar[PldRx] = ContextVar(
|
|
||||||
'pld_rx',
|
|
||||||
default=_def_pld_rx,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def current_pldrx() -> PldRx:
|
|
||||||
'''
|
|
||||||
Return the current `trio.Task.context`'s msg-payload-receiver.
|
|
||||||
|
|
||||||
A payload receiver is the IPC-msg processing sub-sys which
|
|
||||||
filters inter-actor-task communicated payload data, i.e. the
|
|
||||||
`PayloadMsg.pld: PayloadT` field value, AFTER it's container
|
|
||||||
shuttlle msg (eg. `Started`/`Yield`/`Return) has been delivered
|
|
||||||
up from `tractor`'s transport layer but BEFORE the data is
|
|
||||||
yielded to application code, normally via an IPC primitive API
|
|
||||||
like, for ex., `pld_data: PayloadT = MsgStream.receive()`.
|
|
||||||
|
|
||||||
Modification of the current payload spec via `limit_plds()`
|
|
||||||
allows a `tractor` application to contextually filter IPC
|
|
||||||
payload content with a type specification as supported by
|
|
||||||
the interchange backend.
|
|
||||||
|
|
||||||
- for `msgspec` see <PUTLINKHERE>.
|
|
||||||
|
|
||||||
NOTE that the `PldRx` itself is a per-`Context` global sub-system
|
|
||||||
that normally does not change other then the applied pld-spec
|
|
||||||
for the current `trio.Task`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# ctx: context = current_ipc_ctx()
|
|
||||||
# return ctx._pld_rx
|
|
||||||
return _ctxvar_PldRx.get()
|
|
||||||
|
|
||||||
|
|
||||||
@cm
|
@cm
|
||||||
def limit_plds(
|
def limit_plds(
|
||||||
spec: Union[Type[Struct]],
|
spec: Union[Type[Struct]],
|
||||||
|
@ -439,29 +421,55 @@ def limit_plds(
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = True
|
__tracebackhide__: bool = True
|
||||||
try:
|
try:
|
||||||
# sanity on orig settings
|
curr_ctx: Context = current_ipc_ctx()
|
||||||
orig_pldrx: PldRx = current_pldrx()
|
rx: PldRx = curr_ctx._pld_rx
|
||||||
orig_pldec: MsgDec = orig_pldrx.pld_dec
|
orig_pldec: MsgDec = rx.pld_dec
|
||||||
|
|
||||||
with orig_pldrx.limit_plds(
|
with rx.limit_plds(
|
||||||
spec=spec,
|
spec=spec,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) as pldec:
|
) as pldec:
|
||||||
log.info(
|
log.runtime(
|
||||||
'Applying payload-decoder\n\n'
|
'Applying payload-decoder\n\n'
|
||||||
f'{pldec}\n'
|
f'{pldec}\n'
|
||||||
)
|
)
|
||||||
yield pldec
|
yield pldec
|
||||||
finally:
|
finally:
|
||||||
log.info(
|
log.runtime(
|
||||||
'Reverted to previous payload-decoder\n\n'
|
'Reverted to previous payload-decoder\n\n'
|
||||||
f'{orig_pldec}\n'
|
f'{orig_pldec}\n'
|
||||||
)
|
)
|
||||||
assert (
|
# sanity on orig settings
|
||||||
(pldrx := current_pldrx()) is orig_pldrx
|
assert rx.pld_dec is orig_pldec
|
||||||
and
|
|
||||||
pldrx.pld_dec is orig_pldec
|
|
||||||
)
|
@acm
|
||||||
|
async def maybe_limit_plds(
|
||||||
|
ctx: Context,
|
||||||
|
spec: Union[Type[Struct]]|None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> MsgDec|None:
|
||||||
|
'''
|
||||||
|
Async compat maybe-payload type limiter.
|
||||||
|
|
||||||
|
Mostly for use inside other internal `@acm`s such that a separate
|
||||||
|
indent block isn't needed when an async one is already being
|
||||||
|
used.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if spec is None:
|
||||||
|
yield None
|
||||||
|
return
|
||||||
|
|
||||||
|
# sanity on scoping
|
||||||
|
curr_ctx: Context = current_ipc_ctx()
|
||||||
|
assert ctx is curr_ctx
|
||||||
|
|
||||||
|
with ctx._pld_rx.limit_plds(spec=spec) as msgdec:
|
||||||
|
yield msgdec
|
||||||
|
|
||||||
|
curr_ctx: Context = current_ipc_ctx()
|
||||||
|
assert ctx is curr_ctx
|
||||||
|
|
||||||
|
|
||||||
async def drain_to_final_msg(
|
async def drain_to_final_msg(
|
||||||
|
@ -543,21 +551,12 @@ async def drain_to_final_msg(
|
||||||
match msg:
|
match msg:
|
||||||
|
|
||||||
# final result arrived!
|
# final result arrived!
|
||||||
case Return(
|
case Return():
|
||||||
# cid=cid,
|
|
||||||
# pld=res,
|
|
||||||
):
|
|
||||||
# ctx._result: Any = res
|
|
||||||
ctx._result: Any = pld
|
|
||||||
log.runtime(
|
log.runtime(
|
||||||
'Context delivered final draining msg:\n'
|
'Context delivered final draining msg:\n'
|
||||||
f'{pretty_struct.pformat(msg)}'
|
f'{pretty_struct.pformat(msg)}'
|
||||||
)
|
)
|
||||||
# XXX: only close the rx mem chan AFTER
|
ctx._result: Any = pld
|
||||||
# a final result is retreived.
|
|
||||||
# if ctx._rx_chan:
|
|
||||||
# await ctx._rx_chan.aclose()
|
|
||||||
# TODO: ^ we don't need it right?
|
|
||||||
result_msg = msg
|
result_msg = msg
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -664,24 +663,6 @@ async def drain_to_final_msg(
|
||||||
result_msg = msg
|
result_msg = msg
|
||||||
break # OOOOOF, yeah obvi we need this..
|
break # OOOOOF, yeah obvi we need this..
|
||||||
|
|
||||||
# XXX we should never really get here
|
|
||||||
# right! since `._deliver_msg()` should
|
|
||||||
# always have detected an {'error': ..}
|
|
||||||
# msg and already called this right!?!
|
|
||||||
# elif error := unpack_error(
|
|
||||||
# msg=msg,
|
|
||||||
# chan=ctx._portal.channel,
|
|
||||||
# hide_tb=False,
|
|
||||||
# ):
|
|
||||||
# log.critical('SHOULD NEVER GET HERE!?')
|
|
||||||
# assert msg is ctx._cancel_msg
|
|
||||||
# assert error.msgdata == ctx._remote_error.msgdata
|
|
||||||
# assert error.ipc_msg == ctx._remote_error.ipc_msg
|
|
||||||
# from .devx._debug import pause
|
|
||||||
# await pause()
|
|
||||||
# ctx._maybe_cancel_and_set_remote_error(error)
|
|
||||||
# ctx._maybe_raise_remote_err(error)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# bubble the original src key error
|
# bubble the original src key error
|
||||||
raise
|
raise
|
||||||
|
|
Loading…
Reference in New Issue