Port `test_nested_multierrors` off `run_in_actor`
Third `test_cancellation.py` group of the `run_in_actor` removal (#477), - `spawn_and_error` fans out each level's erroring one-shots as concurrent `to_actor.run(fn, an=an)` tasks in a local `trio` task-nursery (recursing per spawner subactor), as does the test-body's top-level spawner loop. - the deterministic exact-breadth nested-BEG shape dies with the legacy teardown-reap: each level now groups whatever subset of sub-tree errors relay before the first one's cancel wins, and a single-member group gets unwrapped by the runtime's own `collapse_eg()` at every actor boundary — so a fully-raced tree relays a bare `RemoteActorError` chain. - loosen the shape walk accordingly: accept a lone `RemoteActorError` or a 1..breadth group whose members box `ExceptionGroup` (multi-relay), `AssertionError` (collapsed leaf chain), `RemoteActorError` (re-boxed collapsed chain) or `BaseExceptionGroup` (runtime reap-deadline `Cancelled` upgrade); fold the windows-only tolerances into the same walk. - raced sibling `trio.Cancelled`s are now ABSORBED by the task-nursery instead of landing in the group, so the MTF shape-mismatch xfail should consistently xpass — note added to drop the marker once CI confirms. - add an `else: pytest.fail()` so a silently-clean tree can no longer pass. Gate: both depths green on `trio` (10 consecutive runs) + `mp_spawn`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-codedrop_ria_nursery
parent
697c6152a6
commit
fa8799d5bb
|
|
@ -450,8 +450,20 @@ async def spawn_and_error(
|
||||||
breadth: int,
|
breadth: int,
|
||||||
depth: int,
|
depth: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
'''
|
||||||
|
Recursively spawn a breadth-wide level of erroring one-shot
|
||||||
|
subactors as concurrent `to_actor.run()` tasks; the leaf level
|
||||||
|
errors ~simultaneously and each level's task-nursery groups
|
||||||
|
whatever `RemoteActorError`s relay before the first one's
|
||||||
|
cancel wins, boxing the (`ExceptionGroup`-shaped) group into
|
||||||
|
this actor's own relayed error.
|
||||||
|
|
||||||
|
'''
|
||||||
name = tractor.current_actor().name
|
name = tractor.current_actor().name
|
||||||
async with tractor.open_nursery() as nursery:
|
async with (
|
||||||
|
tractor.open_nursery() as an,
|
||||||
|
trio.open_nursery() as tn,
|
||||||
|
):
|
||||||
for i in range(breadth):
|
for i in range(breadth):
|
||||||
|
|
||||||
if depth > 0:
|
if depth > 0:
|
||||||
|
|
@ -471,7 +483,14 @@ async def spawn_and_error(
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'name': f'{name}_errorer_{i}',
|
'name': f'{name}_errorer_{i}',
|
||||||
}
|
}
|
||||||
await nursery.run_in_actor(*args, **kwargs)
|
tn.start_soon(
|
||||||
|
partial(
|
||||||
|
tractor.to_actor.run,
|
||||||
|
*args,
|
||||||
|
an=an,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# NOTE: `main_thread_forkserver` capture-fd hang class is no
|
# NOTE: `main_thread_forkserver` capture-fd hang class is no
|
||||||
|
|
@ -513,7 +532,11 @@ async def test_nested_multierrors(
|
||||||
depth: int,
|
depth: int,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Test that failed actor sets are wrapped in `BaseExceptionGroup`s.
|
Test that a nested tree of concurrently failing one-shot
|
||||||
|
subactors tears down cleanly, relaying (whatever subset of)
|
||||||
|
the leaf `AssertionError`s (that win the per-level
|
||||||
|
relay-vs-cancel race) re-boxed/grouped at each actor
|
||||||
|
boundary.
|
||||||
|
|
||||||
Parametrized over recursion `depth ∈ {1, 3}`:
|
Parametrized over recursion `depth ∈ {1, 3}`:
|
||||||
|
|
||||||
|
|
@ -563,6 +586,13 @@ async def test_nested_multierrors(
|
||||||
# fork-spawn jitter + UDS-contention widens both `t1` and
|
# fork-spawn jitter + UDS-contention widens both `t1` and
|
||||||
# `t2` further.
|
# `t2` further.
|
||||||
#
|
#
|
||||||
|
# NB post-#477 (`to_actor.run()` fan-out in a local
|
||||||
|
# task-nursery) a race-tripped sibling's `Cancelled` is
|
||||||
|
# ABSORBED by the task-nursery instead of landing in the
|
||||||
|
# group — the raced case now shows as a *smaller* BEG, so
|
||||||
|
# this marker should consistently `xpass`; drop it once CI
|
||||||
|
# confirms.
|
||||||
|
#
|
||||||
# With `strict=False` the clean-cascade cases (most
|
# With `strict=False` the clean-cascade cases (most
|
||||||
# depth=1 runs, rare depth=3 runs) report as `xpassed`
|
# depth=1 runs, rare depth=3 runs) report as `xpassed`
|
||||||
# while the race-tripped cases report as `xfailed` —
|
# while the race-tripped cases report as `xfailed` —
|
||||||
|
|
@ -667,67 +697,82 @@ async def test_nested_multierrors(
|
||||||
|
|
||||||
async with fail_after_w_trace(timeout):
|
async with fail_after_w_trace(timeout):
|
||||||
try:
|
try:
|
||||||
async with tractor.open_nursery() as nursery:
|
async with (
|
||||||
|
tractor.open_nursery() as nursery,
|
||||||
|
trio.open_nursery() as tn,
|
||||||
|
):
|
||||||
for i in range(subactor_breadth):
|
for i in range(subactor_breadth):
|
||||||
await nursery.run_in_actor(
|
tn.start_soon(
|
||||||
spawn_and_error,
|
partial(
|
||||||
name=f'spawner_{i}',
|
tractor.to_actor.run,
|
||||||
breadth=subactor_breadth,
|
spawn_and_error,
|
||||||
depth=depth,
|
an=nursery,
|
||||||
|
name=f'spawner_{i}',
|
||||||
|
breadth=subactor_breadth,
|
||||||
|
depth=depth,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except BaseExceptionGroup as err:
|
except (
|
||||||
assert len(err.exceptions) == subactor_breadth
|
BaseExceptionGroup,
|
||||||
for subexc in err.exceptions:
|
tractor.RemoteActorError,
|
||||||
|
) as err:
|
||||||
# verify first level actor errors are wrapped as remote
|
# group membership is bounded by the relay-vs-cancel
|
||||||
if _friggin_windows:
|
# race: the first spawner-tree's error cancels its
|
||||||
|
# siblings, whose own errors only group when relayed
|
||||||
|
# first; a fully-raced tree even collapses (via the
|
||||||
|
# runtime's own `collapse_eg()` unwrapping each level's
|
||||||
|
# single-member group) to a bare `RemoteActorError`
|
||||||
|
# re-boxing the leaf `AssertionError` at every actor
|
||||||
|
# boundary. The deterministic exact-breadth nested-BEG
|
||||||
|
# was the legacy `run_in_actor()` reap-all-at-teardown.
|
||||||
|
subexcs: list[BaseException] = (
|
||||||
|
err.exceptions
|
||||||
|
if isinstance(err, BaseExceptionGroup)
|
||||||
|
else [err]
|
||||||
|
)
|
||||||
|
assert 1 <= len(subexcs) <= subactor_breadth
|
||||||
|
for subexc in subexcs:
|
||||||
|
if (
|
||||||
|
_friggin_windows
|
||||||
|
and
|
||||||
|
isinstance(subexc, trio.Cancelled)
|
||||||
|
):
|
||||||
# windows is often too slow and cancellation seems
|
# windows is often too slow and cancellation seems
|
||||||
# to happen before an actor is spawned
|
# to happen before an actor is spawned
|
||||||
if isinstance(subexc, trio.Cancelled):
|
continue
|
||||||
continue
|
|
||||||
|
|
||||||
elif isinstance(subexc, tractor.RemoteActorError):
|
assert isinstance(subexc, tractor.RemoteActorError)
|
||||||
# on windows it seems we can't exactly be sure wtf
|
|
||||||
# will happen..
|
|
||||||
assert subexc.boxed_type in (
|
|
||||||
tractor.RemoteActorError,
|
|
||||||
trio.Cancelled,
|
|
||||||
BaseExceptionGroup,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif isinstance(subexc, BaseExceptionGroup):
|
accepted: tuple[Type[BaseException], ...] = (
|
||||||
for subsub in subexc.exceptions:
|
# ≥2 sub-tree errors relayed before the
|
||||||
|
# cancel-cascade won → grouped per-level.
|
||||||
if subsub in (tractor.RemoteActorError,):
|
ExceptionGroup,
|
||||||
subsub = subsub.boxed_type
|
# every level collapsed down to its lone
|
||||||
|
# relayed (leaf) error.
|
||||||
assert type(subsub) in (
|
AssertionError,
|
||||||
trio.Cancelled,
|
# a mid-level spawner relays an
|
||||||
BaseExceptionGroup,
|
# already-boxed (collapsed) leaf chain,
|
||||||
)
|
# re-boxing the `RemoteActorError` itself.
|
||||||
else:
|
tractor.RemoteActorError,
|
||||||
assert isinstance(subexc, tractor.RemoteActorError)
|
# under heavy load a runtime-internal reap
|
||||||
|
# deadline can inject a `trio.Cancelled`
|
||||||
if depth > 0 and subactor_breadth > 1:
|
# into a child's group before relay (the
|
||||||
# XXX not sure what's up with this..
|
# same class the depth=3 throttle-xfail
|
||||||
# on windows sometimes spawning is just too slow and
|
# covers) upgrading it from an
|
||||||
# we get back the (sent) cancel signal instead
|
# `ExceptionGroup`.
|
||||||
if _friggin_windows:
|
BaseExceptionGroup,
|
||||||
if isinstance(subexc, tractor.RemoteActorError):
|
)
|
||||||
assert subexc.boxed_type in (
|
if _friggin_windows:
|
||||||
BaseExceptionGroup,
|
# on windows it seems we can't exactly be
|
||||||
tractor.RemoteActorError
|
# sure wtf will happen..
|
||||||
)
|
accepted += (
|
||||||
else:
|
trio.Cancelled,
|
||||||
assert isinstance(subexc, BaseExceptionGroup)
|
|
||||||
else:
|
|
||||||
assert subexc.boxed_type is ExceptionGroup
|
|
||||||
else:
|
|
||||||
assert subexc.boxed_type in (
|
|
||||||
tractor.RemoteActorError,
|
|
||||||
trio.Cancelled
|
|
||||||
)
|
)
|
||||||
|
assert subexc.boxed_type in accepted
|
||||||
|
else:
|
||||||
|
pytest.fail(
|
||||||
|
'Should have raised a (grouped) `RemoteActorError`?'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@no_windows
|
@no_windows
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue