From d01a21239cb15b1fd25c486686243f0230c9205f Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 6 Jul 2026 11:18:11 -0400 Subject: [PATCH] Port `test_some_cancels_all` off `run_in_actor` Second `test_cancellation.py` group of the `run_in_actor` removal (#477), - one-shot subactors now run as concurrent `to_actor.run(fn, an=an)` tasks in a local `trio` task-nursery, so their errors raise WHILE the actor-nursery block is open (vs the legacy teardown-reap) and the first error cancels sibling one-shots. - wrap the task-nursery in `collapse_eg()` so the deterministic single-error cases still surface a bare `RemoteActorError`. - loosen the group-shape assertion: the relay-vs-cancel race populates anywhere from 1 to `num_actors` `RemoteActorError`s (the exact-`num_actors` BEG was `run_in_actor`'s reap-all-at-teardown); group members are always `RemoteActorError` now since sibling `trio.Cancelled`s are absorbed by the task-nursery. - move the daemon-portal call loop inside the task-nursery body so the sleep-forever one-shot case is cancelled by the daemon error raise. - rename the `*run_in_actor*` param ids to `*one_shot*`. Gate: 6 passed on both `trio` + `mp_spawn` backends. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tests/test_cancellation.py | 123 ++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 48 deletions(-) diff --git a/tests/test_cancellation.py b/tests/test_cancellation.py index b3720c5e..aa61598b 100644 --- a/tests/test_cancellation.py +++ b/tests/test_cancellation.py @@ -2,6 +2,7 @@ Cancellation and error propagation """ +from functools import partial import os import signal import platform @@ -16,7 +17,10 @@ from tractor._testing import ( tractor_test, ) from tractor._testing.trace import FailAfterWTraceFactory -from tractor.trionics import gather_contexts +from tractor.trionics import ( + collapse_eg, + gather_contexts, +) from .conftest import no_windows @@ -305,30 +309,30 @@ async def test_cancel_infinite_streamer( @pytest.mark.parametrize( 'num_actors_and_errs', [ - # daemon actors sit idle while single task actors error out + # daemon actors sit idle while one-shot task actors error out (1, tractor.RemoteActorError, AssertionError, (assert_err, {}), None), (2, BaseExceptionGroup, AssertionError, (assert_err, {}), None), (3, BaseExceptionGroup, AssertionError, (assert_err, {}), None), - # 1 daemon actor errors out while single task actors sleep forever + # 1 daemon actor errors out while one-shot task actors sleep forever (3, tractor.RemoteActorError, AssertionError, (sleep_forever, {}), (assert_err, {}, True)), - # daemon actors error out after brief delay while single task + # daemon actors error out after brief delay while one-shot task # actors complete quickly (3, tractor.RemoteActorError, AssertionError, (do_nuthin, {}), (assert_err, {'delay': 1}, True)), - # daemon complete quickly delay while single task + # daemon complete quickly delay while one-shot task # actors error after brief delay (3, BaseExceptionGroup, AssertionError, (assert_err, {'delay': 1}), (do_nuthin, {}, False)), ], ids=[ - '1_run_in_actor_fails', - '2_run_in_actors_fail', - '3_run_in_actors_fail', + '1_one_shot_fails', + '2_one_shots_fail', + '3_one_shots_fail', '1_daemon_actors_fail', - '1_daemon_actors_fail_all_run_in_actors_dun_quick', - 'no_daemon_actors_fail_all_run_in_actors_sleep_then_fail', + '1_daemon_actors_fail_all_one_shots_dun_quick', + 'no_daemon_actors_fail_all_one_shots_sleep_then_fail', ], ) @tractor_test( @@ -347,12 +351,22 @@ async def test_some_cancels_all( This is the first and only supervisory strategy at the moment. + One-shot subactors run as concurrent `to_actor.run()` tasks + in a local task-nursery so their errors raise WHILE the + actor-nursery block is still open (vs the legacy + `run_in_actor()` teardown-reap); the first error cancels the + sibling one-shots (whose `trio.Cancelled`s the task-nursery + absorbs) so the group shape is 1..num_actors + `RemoteActorError`s depending on relay-vs-cancel timing — + with `collapse_eg()` unwrapping the deterministic + single-error cases to a bare `RemoteActorError`. + ''' ( num_actors, first_err, err_type, - ria_func, + one_shot_func, da_func, ) = num_actors_and_errs try: @@ -366,51 +380,64 @@ async def test_some_cancels_all( enable_modules=[__name__], )) - func, kwargs = ria_func - riactor_portals = [] - for i in range(num_actors): - # start actor(s) that will fail immediately - riactor_portals.append( - await an.run_in_actor( - func, - name=f'actor_{i}', - **kwargs + func, kwargs = one_shot_func + async with ( + collapse_eg(), + trio.open_nursery() as tn, + ): + for i in range(num_actors): + # schedule one-shot task actor(s); errors + # raise into this task-nursery scope. + tn.start_soon( + partial( + tractor.to_actor.run, + func, + an=an, + name=f'actor_{i}', + **kwargs, + ) ) - ) - if da_func: - func, kwargs, expect_error = da_func - for portal in dactor_portals: - # if this function fails then we should error here - # and the nursery should teardown all other actors - try: - await portal.run(func, **kwargs) + if da_func: + func, kwargs, expect_error = da_func + for portal in dactor_portals: + # if this function fails then we should error + # here and the nursery should teardown all + # other actors + try: + await portal.run(func, **kwargs) - except tractor.RemoteActorError as err: - assert err.boxed_type == err_type - # we only expect this first error to propogate - # (all other daemons are cancelled before they - # can be scheduled) - num_actors = 1 - # reraise so nursery teardown is triggered - raise - else: - if expect_error: - pytest.fail( - "Deamon call should fail at checkpoint?") + except tractor.RemoteActorError as err: + assert err.boxed_type == err_type + # we only expect this first error to propogate + # (all other daemons are cancelled before they + # can be scheduled) + num_actors = 1 + # reraise so nursery teardown is triggered + raise + else: + if expect_error: + pytest.fail( + "Deamon call should fail at checkpoint?") - # should error here with a ``RemoteActorError`` or ``MultiError`` + # should error here with a `RemoteActorError` or a beg of them - except first_err as _err: + except ( + BaseExceptionGroup, + tractor.RemoteActorError, + ) as _err: err = _err if isinstance(err, BaseExceptionGroup): - assert len(err.exceptions) == num_actors + # only the concurrent multi-error cases can group; the + # relay-vs-cancel race means anywhere from 1 (all + # siblings cancelled before relaying) up to all + # `num_actors` errors may populate the group. + assert first_err is BaseExceptionGroup + assert 1 <= len(err.exceptions) <= num_actors for exc in err.exceptions: - if isinstance(exc, tractor.RemoteActorError): - assert exc.boxed_type == err_type - else: - assert isinstance(exc, trio.Cancelled) - elif isinstance(err, tractor.RemoteActorError): + assert isinstance(exc, tractor.RemoteActorError) + assert exc.boxed_type == err_type + else: assert err.boxed_type == err_type assert an.cancel_called is True