|
|
|
@ -0,0 +1,260 @@
|
|
|
|
|
'''
|
|
|
|
|
Special case testing for issues not (dis)covered in the primary
|
|
|
|
|
`Context` related functional/scenario suites.
|
|
|
|
|
|
|
|
|
|
**NOTE: this mod is a WIP** space for handling
|
|
|
|
|
odd/rare/undiscovered/not-yet-revealed faults which either
|
|
|
|
|
loudly (ideal case) breakl our supervision protocol
|
|
|
|
|
or (worst case) result in distributed sys hangs.
|
|
|
|
|
|
|
|
|
|
Suites here further try to clarify (if [partially] ill-defined) and
|
|
|
|
|
verify our edge case semantics for inter-actor-relayed-exceptions
|
|
|
|
|
including,
|
|
|
|
|
|
|
|
|
|
- lowlevel: what remote obj-data is interchanged for IPC and what is
|
|
|
|
|
native-obj form is expected from unpacking in the the new
|
|
|
|
|
mem-domain.
|
|
|
|
|
|
|
|
|
|
- which kinds of `RemoteActorError` (and its derivs) are expected by which
|
|
|
|
|
(types of) peers (parent, child, sibling, etc) with what
|
|
|
|
|
particular meta-data set such as,
|
|
|
|
|
|
|
|
|
|
- `.src_uid`: the original (maybe) peer who raised.
|
|
|
|
|
- `.relay_uid`: the next-hop-peer who sent it.
|
|
|
|
|
- `.relay_path`: the sequence of peer actor hops.
|
|
|
|
|
- `.is_inception`: a predicate that denotes multi-hop remote errors.
|
|
|
|
|
|
|
|
|
|
- when should `ExceptionGroup`s be relayed from a particular
|
|
|
|
|
remote endpoint, they should never be caused by implicit `._rpc`
|
|
|
|
|
nursery machinery!
|
|
|
|
|
|
|
|
|
|
- various special `trio` edge cases around its cancellation semantics
|
|
|
|
|
and how we (currently) leverage `trio.Cancelled` as a signal for
|
|
|
|
|
whether a `Context` task should raise `ContextCancelled` (ctx).
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
# from contextlib import (
|
|
|
|
|
# asynccontextmanager as acm,
|
|
|
|
|
# )
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
import trio
|
|
|
|
|
import tractor
|
|
|
|
|
from tractor import ( # typing
|
|
|
|
|
ActorNursery,
|
|
|
|
|
Portal,
|
|
|
|
|
Context,
|
|
|
|
|
ContextCancelled,
|
|
|
|
|
)
|
|
|
|
|
# from tractor._testing import (
|
|
|
|
|
# tractor_test,
|
|
|
|
|
# expect_ctxc,
|
|
|
|
|
# )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@tractor.context
|
|
|
|
|
async def sleep_n_chkpt_in_finally(
|
|
|
|
|
ctx: Context,
|
|
|
|
|
sleep_n_raise: bool,
|
|
|
|
|
chld_raise_delay: float,
|
|
|
|
|
chld_finally_delay: float,
|
|
|
|
|
rent_cancels: bool,
|
|
|
|
|
rent_ctxc_delay: float,
|
|
|
|
|
tn_cancels: bool,
|
|
|
|
|
gto_task: bool = False,
|
|
|
|
|
|
|
|
|
|
tn_cancels: bool = False,
|
|
|
|
|
expect_exc: str|None = None,
|
|
|
|
|
|
|
|
|
|
) -> None:
|
|
|
|
|
'''
|
|
|
|
|
Sync, open a tn, then wait for cancel, run a chkpt inside
|
|
|
|
|
the user's `finally:` teardown.
|
|
|
|
|
|
|
|
|
|
This covers a footgun case that `trio` core doesn't seem to care about
|
|
|
|
|
wherein an exc can be masked by a `trio.Cancelled` raised inside a tn emedded
|
|
|
|
|
`finally:`.
|
|
|
|
|
|
|
|
|
|
Also see `test_trioisms::test_acm_embedded_nursery_propagates_enter_err`
|
|
|
|
|
for the down and gritty details.
|
|
|
|
|
|
|
|
|
|
Since a `@context` endpoint fn can also contain code like this,
|
|
|
|
|
**and** bc we currently have no easy way other then
|
|
|
|
|
`trio.Cancelled` to signal cancellation on each side of an IPC `Context`,
|
|
|
|
|
the footgun issue can compound itself as demonstrated in this suite..
|
|
|
|
|
|
|
|
|
|
Here are some edge cases codified with "sclang" syntax.
|
|
|
|
|
Note that the parent/child relationship is just a pragmatic
|
|
|
|
|
choice, these cases can occurr regardless of the supervision
|
|
|
|
|
hiearchy,
|
|
|
|
|
|
|
|
|
|
- rent c)=> chld.raises-then-taskc-in-finally
|
|
|
|
|
|_ chld's body raises an `exc: BaseException`.
|
|
|
|
|
_ in its `finally:` block it runs a chkpoint
|
|
|
|
|
which raises a taskc (`trio.Cancelled`) which
|
|
|
|
|
masks `exc` instead raising taskc up to the first tn.
|
|
|
|
|
_ the embedded/chld tn captures the masking taskc and then
|
|
|
|
|
raises it up to the ._rpc-ep-tn instead of `exc`.
|
|
|
|
|
_ the rent thinks the child ctxc-ed instead of errored..
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
await ctx.started()
|
|
|
|
|
|
|
|
|
|
if expect_exc:
|
|
|
|
|
expect_exc: BaseException = tractor._exceptions.get_err_type(
|
|
|
|
|
type_name=expect_exc,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
berr: BaseException|None = None
|
|
|
|
|
async with (
|
|
|
|
|
tractor.trionics.collapse_eg(
|
|
|
|
|
# raise_from_src=True, # to show orig eg
|
|
|
|
|
),
|
|
|
|
|
trio.open_nursery() as tn
|
|
|
|
|
):
|
|
|
|
|
if gto_task:
|
|
|
|
|
tn.start_soon(trio.sleep_forever())
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if not sleep_n_raise:
|
|
|
|
|
await trio.sleep_forever()
|
|
|
|
|
elif sleep_n_raise:
|
|
|
|
|
|
|
|
|
|
# XXX this sleep is less then the sleep the parent
|
|
|
|
|
# does before calling `ctx.cancel()`
|
|
|
|
|
await trio.sleep(chld_raise_delay)
|
|
|
|
|
|
|
|
|
|
# XXX this will be masked by a taskc raised in
|
|
|
|
|
# the `finally:` if this fn doesn't terminate
|
|
|
|
|
# before any ctxc-req arrives AND a checkpoint is hit
|
|
|
|
|
# in that `finally:`.
|
|
|
|
|
raise RuntimeError('my app krurshed..')
|
|
|
|
|
|
|
|
|
|
except BaseException as _berr:
|
|
|
|
|
berr = _berr
|
|
|
|
|
|
|
|
|
|
# TODO: it'd sure be nice to be able to inject our own
|
|
|
|
|
# `ContextCancelled` here instead of of `trio.Cancelled`
|
|
|
|
|
# so that our runtime can expect it and this "user code"
|
|
|
|
|
# would be able to tell the diff between a generic trio
|
|
|
|
|
# cancel and a tractor runtime-IPC cancel.
|
|
|
|
|
if expect_exc:
|
|
|
|
|
if not isinstance(
|
|
|
|
|
berr,
|
|
|
|
|
expect_exc,
|
|
|
|
|
):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f'Unexpected exc type ??\n'
|
|
|
|
|
f'{berr!r}\n'
|
|
|
|
|
f'\n'
|
|
|
|
|
f'Expected a {expect_exc!r}\n'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
raise berr
|
|
|
|
|
|
|
|
|
|
# simulate what user code might try even though
|
|
|
|
|
# it's a known boo-boo..
|
|
|
|
|
finally:
|
|
|
|
|
# maybe wait for rent ctxc to arrive
|
|
|
|
|
with trio.CancelScope(shield=True):
|
|
|
|
|
await trio.sleep(chld_finally_delay)
|
|
|
|
|
|
|
|
|
|
if tn_cancels:
|
|
|
|
|
tn.cancel_scope.cancel()
|
|
|
|
|
|
|
|
|
|
# !!XXX this will raise `trio.Cancelled` which
|
|
|
|
|
# will mask the RTE from above!!!
|
|
|
|
|
#
|
|
|
|
|
# YES, it's the same case as our extant
|
|
|
|
|
# `test_trioisms::test_acm_embedded_nursery_propagates_enter_err`
|
|
|
|
|
try:
|
|
|
|
|
await trio.lowlevel.checkpoint()
|
|
|
|
|
except trio.Cancelled as taskc:
|
|
|
|
|
if (scope_err := taskc.__context__):
|
|
|
|
|
print(
|
|
|
|
|
f'XXX MASKED REMOTE ERROR XXX\n'
|
|
|
|
|
f'ENDPOINT exception -> {scope_err!r}\n'
|
|
|
|
|
f'will be masked by -> {taskc!r}\n'
|
|
|
|
|
)
|
|
|
|
|
# await tractor.pause(shield=True)
|
|
|
|
|
|
|
|
|
|
raise taskc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
'chld_callspec',
|
|
|
|
|
[
|
|
|
|
|
dict(
|
|
|
|
|
sleep_n_raise=None,
|
|
|
|
|
chld_raise_delay=0.1,
|
|
|
|
|
chld_finally_delay=0.1,
|
|
|
|
|
expect_exc='Cancelled',
|
|
|
|
|
rent_cancels=True,
|
|
|
|
|
rent_ctxc_delay=0.1,
|
|
|
|
|
tn_cancels=True,
|
|
|
|
|
),
|
|
|
|
|
dict(
|
|
|
|
|
sleep_n_raise='RuntimeError',
|
|
|
|
|
chld_raise_delay=0.1,
|
|
|
|
|
chld_finally_delay=1,
|
|
|
|
|
expect_exc='RuntimeError',
|
|
|
|
|
rent_cancels=False,
|
|
|
|
|
rent_ctxc_delay=0.1,
|
|
|
|
|
tn_cancels=False,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
ids=lambda item: f'chld_callspec={item!r}'
|
|
|
|
|
)
|
|
|
|
|
def test_masked_taskc_with_taskc_still_is_contx(
|
|
|
|
|
debug_mode: bool,
|
|
|
|
|
chld_callspec: dict,
|
|
|
|
|
tpt_proto: str,
|
|
|
|
|
):
|
|
|
|
|
expect_exc_str: str|None = chld_callspec['sleep_n_raise']
|
|
|
|
|
rent_ctxc_delay: float|None = chld_callspec['rent_ctxc_delay']
|
|
|
|
|
async def main():
|
|
|
|
|
an: ActorNursery
|
|
|
|
|
async with tractor.open_nursery(
|
|
|
|
|
debug_mode=debug_mode,
|
|
|
|
|
enable_transports=[tpt_proto],
|
|
|
|
|
) as an:
|
|
|
|
|
ptl: Portal = await an.start_actor(
|
|
|
|
|
'cancellee',
|
|
|
|
|
enable_modules=[__name__],
|
|
|
|
|
)
|
|
|
|
|
ctx: Context
|
|
|
|
|
async with (
|
|
|
|
|
ptl.open_context(
|
|
|
|
|
sleep_n_chkpt_in_finally,
|
|
|
|
|
**chld_callspec,
|
|
|
|
|
) as (ctx, sent),
|
|
|
|
|
):
|
|
|
|
|
assert not sent
|
|
|
|
|
await trio.sleep(rent_ctxc_delay)
|
|
|
|
|
await ctx.cancel()
|
|
|
|
|
|
|
|
|
|
# recv error or result from chld
|
|
|
|
|
ctxc: ContextCancelled = await ctx.wait_for_result()
|
|
|
|
|
assert (
|
|
|
|
|
ctxc is ctx.outcome
|
|
|
|
|
and
|
|
|
|
|
isinstance(ctxc, ContextCancelled)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# always graceful terminate the sub in non-error cases
|
|
|
|
|
await an.cancel()
|
|
|
|
|
|
|
|
|
|
if expect_exc_str:
|
|
|
|
|
expect_exc: BaseException = tractor._exceptions.get_err_type(
|
|
|
|
|
type_name=expect_exc_str,
|
|
|
|
|
)
|
|
|
|
|
with pytest.raises(
|
|
|
|
|
expected_exception=tractor.RemoteActorError,
|
|
|
|
|
) as excinfo:
|
|
|
|
|
trio.run(main)
|
|
|
|
|
|
|
|
|
|
rae = excinfo.value
|
|
|
|
|
assert expect_exc == rae.boxed_type
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
trio.run(main)
|