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-code
drop_ria_nursery
Gud Boi 2026-07-06 11:47:47 -04:00
parent 697c6152a6
commit fa8799d5bb
1 changed files with 102 additions and 57 deletions

View File

@ -450,8 +450,20 @@ async def spawn_and_error(
breadth: int,
depth: int,
) -> 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
async with tractor.open_nursery() as nursery:
async with (
tractor.open_nursery() as an,
trio.open_nursery() as tn,
):
for i in range(breadth):
if depth > 0:
@ -471,7 +483,14 @@ async def spawn_and_error(
kwargs = {
'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
@ -513,7 +532,11 @@ async def test_nested_multierrors(
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}`:
@ -563,6 +586,13 @@ async def test_nested_multierrors(
# fork-spawn jitter + UDS-contention widens both `t1` and
# `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
# depth=1 runs, rare depth=3 runs) report as `xpassed`
# while the race-tripped cases report as `xfailed` —
@ -667,66 +697,81 @@ async def test_nested_multierrors(
async with fail_after_w_trace(timeout):
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):
await nursery.run_in_actor(
tn.start_soon(
partial(
tractor.to_actor.run,
spawn_and_error,
an=nursery,
name=f'spawner_{i}',
breadth=subactor_breadth,
depth=depth,
)
except BaseExceptionGroup as err:
assert len(err.exceptions) == subactor_breadth
for subexc in err.exceptions:
# verify first level actor errors are wrapped as remote
if _friggin_windows:
)
except (
BaseExceptionGroup,
tractor.RemoteActorError,
) as err:
# group membership is bounded by the relay-vs-cancel
# 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
# to happen before an actor is spawned
if isinstance(subexc, trio.Cancelled):
continue
elif 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):
for subsub in subexc.exceptions:
if subsub in (tractor.RemoteActorError,):
subsub = subsub.boxed_type
assert type(subsub) in (
trio.Cancelled,
BaseExceptionGroup,
)
else:
assert isinstance(subexc, tractor.RemoteActorError)
if depth > 0 and subactor_breadth > 1:
# XXX not sure what's up with this..
# on windows sometimes spawning is just too slow and
# we get back the (sent) cancel signal instead
if _friggin_windows:
if isinstance(subexc, tractor.RemoteActorError):
assert subexc.boxed_type in (
BaseExceptionGroup,
tractor.RemoteActorError
)
else:
assert isinstance(subexc, BaseExceptionGroup)
else:
assert subexc.boxed_type is ExceptionGroup
else:
assert subexc.boxed_type in (
accepted: tuple[Type[BaseException], ...] = (
# ≥2 sub-tree errors relayed before the
# cancel-cascade won → grouped per-level.
ExceptionGroup,
# every level collapsed down to its lone
# relayed (leaf) error.
AssertionError,
# a mid-level spawner relays an
# already-boxed (collapsed) leaf chain,
# re-boxing the `RemoteActorError` itself.
tractor.RemoteActorError,
trio.Cancelled
# under heavy load a runtime-internal reap
# deadline can inject a `trio.Cancelled`
# into a child's group before relay (the
# same class the depth=3 throttle-xfail
# covers) upgrading it from an
# `ExceptionGroup`.
BaseExceptionGroup,
)
if _friggin_windows:
# on windows it seems we can't exactly be
# sure wtf will happen..
accepted += (
trio.Cancelled,
)
assert subexc.boxed_type in accepted
else:
pytest.fail(
'Should have raised a (grouped) `RemoteActorError`?'
)