Adjust ep-masking-suite for the real-use-case

Namely that the more common-and-pertinent case is when
a `@context`-ep-fn contains the `finally`-footgun but without
a surrounding embedded `tn` (which currently still requires its own
scope embedded `trionics.maybe_raise_from_masking_exc()`) which can't
be compensated-for by `._rpc._invoke()` easily. Instead the test is
composed where the `._invoke()`-internal `tn` is the machinery being
addressed in terms of masking user-code excs with `trio.Cancelled`.

Deats,
- rename the test -> `test_unmasked_remote_exc` to reflect what the
  runtime should actually be addressing/solving.
- drop the embedded `tn` from `sleep_n_chkpt_in_finally()` (for now)
  since that case can't currently easily be addressed without the user
  code using its own `trionics.maybe_raise_from_masking_exc()` inside
  the nursery scope.
- as such drop all `tn` related params/logic/usage from the ep.
- add in a `Cancelled` handler block which checks for RTE masking and
  always prints the occurrence loudly.

Follow up,
- obvi this suite will currently fail until the appropriate adjustment
  is made to `._rpc._invoke()` to do the unmasking; coming next.
- we probably still need a case with an embedded user `tn` where if
  the default strict-eg mode is used then a ctxc from the parent might
  cause a non-graceful `Context.cancel()` outcome?
 |_since the embedded user-`tn` will raise
   `ExceptionGroup[trio.Cancelled]` upward despite the parent nursery's
   scope being the canceller, or will a `collapse_eg()` inside the
   `._invoke()` scope handle this as well?
moar_eg_smoothing
Tyler Goodlet 2025-06-15 19:18:30 -04:00
parent 86346c27e8
commit bad42734db
1 changed files with 65 additions and 87 deletions

View File

@ -33,10 +33,6 @@ including,
whether a `Context` task should raise `ContextCancelled` (ctx). whether a `Context` task should raise `ContextCancelled` (ctx).
''' '''
# from contextlib import (
# asynccontextmanager as acm,
# )
import pytest import pytest
import trio import trio
import tractor import tractor
@ -46,23 +42,19 @@ from tractor import ( # typing
Context, Context,
ContextCancelled, ContextCancelled,
) )
# from tractor._testing import (
# tractor_test,
# expect_ctxc,
# )
@tractor.context @tractor.context
async def sleep_n_chkpt_in_finally( async def sleep_n_chkpt_in_finally(
ctx: Context, ctx: Context,
sleep_n_raise: bool, sleep_n_raise: bool,
chld_raise_delay: float, chld_raise_delay: float,
chld_finally_delay: float, chld_finally_delay: float,
rent_cancels: bool, rent_cancels: bool,
rent_ctxc_delay: float, rent_ctxc_delay: float,
gto_task: bool = False,
tn_cancels: bool = False,
expect_exc: str|None = None, expect_exc: str|None = None,
) -> None: ) -> None:
@ -82,10 +74,10 @@ async def sleep_n_chkpt_in_finally(
`trio.Cancelled` to signal cancellation on each side of an IPC `Context`, `trio.Cancelled` to signal cancellation on each side of an IPC `Context`,
the footgun issue can compound itself as demonstrated in this suite.. the footgun issue can compound itself as demonstrated in this suite..
Here are some edge cases codified with "sclang" syntax. Here are some edge cases codified with our WIP "sclang" syntax
Note that the parent/child relationship is just a pragmatic (note the parent(rent)/child(chld) naming here is just
choice, these cases can occurr regardless of the supervision pragmatism, generally these most of these cases can occurr
hiearchy, regardless of the distributed-task's supervision hiearchy),
- rent c)=> chld.raises-then-taskc-in-finally - rent c)=> chld.raises-then-taskc-in-finally
|_ chld's body raises an `exc: BaseException`. |_ chld's body raises an `exc: BaseException`.
@ -105,79 +97,67 @@ async def sleep_n_chkpt_in_finally(
) )
berr: BaseException|None = None berr: BaseException|None = None
async with ( try:
tractor.trionics.collapse_eg( if not sleep_n_raise:
# raise_from_src=True, # to show orig eg await trio.sleep_forever()
), elif sleep_n_raise:
trio.open_nursery() as tn
):
if gto_task:
tn.start_soon(trio.sleep_forever())
# 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)
# !!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: try:
if not sleep_n_raise: await trio.lowlevel.checkpoint()
await trio.sleep_forever() except trio.Cancelled as taskc:
elif sleep_n_raise: 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)
# XXX this sleep is less then the sleep the parent raise taskc
# 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( @pytest.mark.parametrize(
@ -190,7 +170,6 @@ async def sleep_n_chkpt_in_finally(
expect_exc='Cancelled', expect_exc='Cancelled',
rent_cancels=True, rent_cancels=True,
rent_ctxc_delay=0.1, rent_ctxc_delay=0.1,
tn_cancels=True,
), ),
dict( dict(
sleep_n_raise='RuntimeError', sleep_n_raise='RuntimeError',
@ -199,12 +178,11 @@ async def sleep_n_chkpt_in_finally(
expect_exc='RuntimeError', expect_exc='RuntimeError',
rent_cancels=False, rent_cancels=False,
rent_ctxc_delay=0.1, rent_ctxc_delay=0.1,
tn_cancels=False,
), ),
], ],
ids=lambda item: f'chld_callspec={item!r}' ids=lambda item: f'chld_callspec={item!r}'
) )
def test_masked_taskc_with_taskc_still_is_contx( def test_unmasked_remote_exc(
debug_mode: bool, debug_mode: bool,
chld_callspec: dict, chld_callspec: dict,
tpt_proto: str, tpt_proto: str,