2018-09-01 18:52:48 +00:00
|
|
|
|
"""
|
|
|
|
|
|
Cancellation and error propagation
|
2021-10-15 13:16:51 +00:00
|
|
|
|
|
2018-09-01 18:52:48 +00:00
|
|
|
|
"""
|
2020-07-21 04:23:14 +00:00
|
|
|
|
import os
|
|
|
|
|
|
import signal
|
2019-11-23 00:27:54 +00:00
|
|
|
|
import platform
|
2020-07-29 17:27:15 +00:00
|
|
|
|
import time
|
2018-09-01 18:52:48 +00:00
|
|
|
|
from itertools import repeat
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
from typing import Type
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
import trio
|
|
|
|
|
|
import tractor
|
2024-03-12 19:48:20 +00:00
|
|
|
|
from tractor._testing import (
|
|
|
|
|
|
tractor_test,
|
|
|
|
|
|
)
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
from tractor._testing.trace import FailAfterWTraceFactory
|
2025-03-12 17:13:20 +00:00
|
|
|
|
from .conftest import no_windows
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-06 17:03:33 +00:00
|
|
|
|
_non_linux: bool = platform.system() != 'Linux'
|
|
|
|
|
|
_friggin_windows: bool = platform.system() == 'Windows'
|
2022-01-20 13:26:30 +00:00
|
|
|
|
|
|
|
|
|
|
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
pytestmark = [
|
|
|
|
|
|
# Multi-actor cancel cascades under
|
|
|
|
|
|
# `--spawn-backend=subint` trip the abandoned-subint
|
|
|
|
|
|
# GIL-hostage class — a stuck subint can starve the
|
|
|
|
|
|
# parent's trio loop and block cancel-delivery.
|
|
|
|
|
|
# Apply the skip module-wide rather than per-test
|
|
|
|
|
|
# since every test here exercises the same cascade.
|
|
|
|
|
|
pytest.mark.skipon_spawn_backend(
|
|
|
|
|
|
'subint',
|
|
|
|
|
|
reason=(
|
|
|
|
|
|
'XXX SUBINT GIL-CONTENTION HANGING TEST XXX\n'
|
|
|
|
|
|
'Cancel cascades under '
|
|
|
|
|
|
'`--spawn-backend=subint` trip the abandoned-subint '
|
|
|
|
|
|
'GIL-hostage class — see\n'
|
|
|
|
|
|
' - `ai/conc-anal/subint_sigint_starvation_issue.md` '
|
|
|
|
|
|
'(GIL-hostage, SIGINT-unresponsive)\n'
|
|
|
|
|
|
' - `ai/conc-anal/subint_cancel_delivery_hang_issue.md` '
|
|
|
|
|
|
'(sibling: parent parks on dead chan)\n'
|
|
|
|
|
|
' - https://github.com/goodboy/tractor/issues/379 '
|
|
|
|
|
|
'(subint umbrella)\n'
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
pytest.mark.usefixtures(
|
|
|
|
|
|
'reap_subactors_per_test',
|
|
|
|
|
|
# NOTE, cancellation tests stress the SIGKILL
|
|
|
|
|
|
# `hard_kill` path which leaks UDS sock-files when
|
|
|
|
|
|
# the subactor's IPC server `finally:` cleanup
|
|
|
|
|
|
# doesn't run. Track per-test for blame attribution.
|
|
|
|
|
|
'track_orphaned_uds_per_test',
|
|
|
|
|
|
# NOTE, cancel-cascade timing races (see
|
|
|
|
|
|
# `test_nested_multierrors`) can also leave a
|
|
|
|
|
|
# subactor spinning at 100% CPU when its cancel
|
|
|
|
|
|
# signal got swallowed mid-handshake. Catches the
|
|
|
|
|
|
# runaway-loop class that doesn't leak UDS socks
|
|
|
|
|
|
# but burns the box.
|
|
|
|
|
|
'detect_runaway_subactors_per_test',
|
|
|
|
|
|
),
|
|
|
|
|
|
]
|
2026-04-22 01:33:15 +00:00
|
|
|
|
|
|
|
|
|
|
|
2019-10-25 20:43:53 +00:00
|
|
|
|
async def assert_err(delay=0):
|
|
|
|
|
|
await trio.sleep(delay)
|
2018-09-01 18:52:48 +00:00
|
|
|
|
assert 0
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-10-25 20:43:53 +00:00
|
|
|
|
async def sleep_forever():
|
2020-07-21 04:23:14 +00:00
|
|
|
|
await trio.sleep_forever()
|
2019-10-25 20:43:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def do_nuthin():
|
|
|
|
|
|
# just nick the scheduler
|
|
|
|
|
|
await trio.sleep(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-11-19 19:16:42 +00:00
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
'args_err',
|
|
|
|
|
|
[
|
|
|
|
|
|
# expected to be thrown in assert_err
|
|
|
|
|
|
({}, AssertionError),
|
|
|
|
|
|
# argument mismatch raised in _invoke()
|
|
|
|
|
|
({'unexpected': 10}, TypeError)
|
|
|
|
|
|
],
|
|
|
|
|
|
ids=['no_args', 'unexpected_args'],
|
|
|
|
|
|
)
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
def test_remote_error(
|
|
|
|
|
|
reg_addr: tuple,
|
|
|
|
|
|
args_err: tuple[dict, Type[Exception]],
|
|
|
|
|
|
set_fork_aware_capture,
|
|
|
|
|
|
):
|
2024-02-28 22:13:01 +00:00
|
|
|
|
'''
|
|
|
|
|
|
Verify an error raised in a subactor that is propagated
|
2018-11-22 16:43:04 +00:00
|
|
|
|
to the parent nursery, contains the underlying boxed builtin
|
|
|
|
|
|
error type info and causes cancellation and reraising all the
|
|
|
|
|
|
way up the stack.
|
2024-02-28 22:13:01 +00:00
|
|
|
|
|
|
|
|
|
|
'''
|
2018-11-19 19:16:42 +00:00
|
|
|
|
args, errtype = args_err
|
|
|
|
|
|
|
2018-09-01 18:52:48 +00:00
|
|
|
|
async def main():
|
2021-02-24 19:37:55 +00:00
|
|
|
|
async with tractor.open_nursery(
|
2025-03-20 21:50:22 +00:00
|
|
|
|
registry_addrs=[reg_addr],
|
2021-02-24 19:37:55 +00:00
|
|
|
|
) as nursery:
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
2022-10-13 21:00:24 +00:00
|
|
|
|
# on a remote type error caused by bad input args
|
|
|
|
|
|
# this should raise directly which means we **don't** get
|
|
|
|
|
|
# an exception group outside the nursery since the error
|
|
|
|
|
|
# here and the far end task error are one in the same?
|
2021-04-28 15:55:37 +00:00
|
|
|
|
portal = await nursery.run_in_actor(
|
2024-02-28 22:13:01 +00:00
|
|
|
|
assert_err,
|
|
|
|
|
|
name='errorer',
|
|
|
|
|
|
**args
|
2021-04-28 15:55:37 +00:00
|
|
|
|
)
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
|
|
|
|
|
# get result(s) from main task
|
|
|
|
|
|
try:
|
2022-10-13 21:00:24 +00:00
|
|
|
|
# this means the root actor will also raise a local
|
|
|
|
|
|
# parent task error and thus an eg will propagate out
|
|
|
|
|
|
# of this actor nursery.
|
2018-11-19 19:16:42 +00:00
|
|
|
|
await portal.result()
|
|
|
|
|
|
except tractor.RemoteActorError as err:
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert err.boxed_type == errtype
|
2018-09-01 18:52:48 +00:00
|
|
|
|
print("Look Maa that actor failed hard, hehh")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
2022-10-13 21:00:24 +00:00
|
|
|
|
# ensure boxed errors
|
|
|
|
|
|
if args:
|
|
|
|
|
|
with pytest.raises(tractor.RemoteActorError) as excinfo:
|
|
|
|
|
|
trio.run(main)
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert excinfo.value.boxed_type == errtype
|
2022-10-13 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
|
else:
|
2024-05-09 20:31:23 +00:00
|
|
|
|
# the root task will also error on the `Portal.result()`
|
|
|
|
|
|
# call so we expect an error from there AND the child.
|
|
|
|
|
|
# |_ tho seems like on new `trio` this doesn't always
|
|
|
|
|
|
# happen?
|
|
|
|
|
|
with pytest.raises((
|
|
|
|
|
|
BaseExceptionGroup,
|
|
|
|
|
|
tractor.RemoteActorError,
|
|
|
|
|
|
)) as excinfo:
|
2022-10-13 21:00:24 +00:00
|
|
|
|
trio.run(main)
|
|
|
|
|
|
|
2024-05-09 20:31:23 +00:00
|
|
|
|
# ensure boxed errors are `errtype`
|
|
|
|
|
|
err: BaseException = excinfo.value
|
|
|
|
|
|
if isinstance(err, BaseExceptionGroup):
|
|
|
|
|
|
suberrs: list[BaseException] = err.exceptions
|
|
|
|
|
|
else:
|
|
|
|
|
|
suberrs: list[BaseException] = [err]
|
|
|
|
|
|
|
|
|
|
|
|
for exc in suberrs:
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert exc.boxed_type == errtype
|
2018-11-22 16:43:04 +00:00
|
|
|
|
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
2024-05-09 20:31:23 +00:00
|
|
|
|
def test_multierror(
|
|
|
|
|
|
reg_addr: tuple[str, int],
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
start_method: str, # parametrized
|
|
|
|
|
|
set_fork_aware_capture, #: Callable,
|
2024-05-09 20:31:23 +00:00
|
|
|
|
):
|
2022-10-09 17:41:08 +00:00
|
|
|
|
'''
|
|
|
|
|
|
Verify we raise a ``BaseExceptionGroup`` out of a nursery where
|
2018-11-19 19:16:42 +00:00
|
|
|
|
more then one actor errors.
|
2022-10-09 17:41:08 +00:00
|
|
|
|
|
|
|
|
|
|
'''
|
2018-11-19 19:16:42 +00:00
|
|
|
|
async def main():
|
2021-02-24 19:37:55 +00:00
|
|
|
|
async with tractor.open_nursery(
|
2025-03-20 21:50:22 +00:00
|
|
|
|
registry_addrs=[reg_addr],
|
2021-02-24 19:37:55 +00:00
|
|
|
|
) as nursery:
|
2018-11-19 19:16:42 +00:00
|
|
|
|
|
2020-12-21 14:09:55 +00:00
|
|
|
|
await nursery.run_in_actor(assert_err, name='errorer1')
|
|
|
|
|
|
portal2 = await nursery.run_in_actor(assert_err, name='errorer2')
|
2018-11-19 19:16:42 +00:00
|
|
|
|
|
|
|
|
|
|
# get result(s) from main task
|
|
|
|
|
|
try:
|
|
|
|
|
|
await portal2.result()
|
|
|
|
|
|
except tractor.RemoteActorError as err:
|
2025-02-25 16:16:01 +00:00
|
|
|
|
assert err.boxed_type is AssertionError
|
2018-11-19 19:16:42 +00:00
|
|
|
|
print("Look Maa that first actor failed hard, hehh")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
2022-10-09 17:41:08 +00:00
|
|
|
|
# here we should get a ``BaseExceptionGroup`` containing exceptions
|
2018-11-19 19:16:42 +00:00
|
|
|
|
# from both subactors
|
|
|
|
|
|
|
2022-10-09 17:41:08 +00:00
|
|
|
|
with pytest.raises(BaseExceptionGroup):
|
2021-02-24 19:37:55 +00:00
|
|
|
|
trio.run(main)
|
2018-11-19 19:16:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
2019-10-30 04:16:39 +00:00
|
|
|
|
@pytest.mark.parametrize(
|
2026-04-22 01:33:15 +00:00
|
|
|
|
'delay',
|
|
|
|
|
|
(0, 0.5),
|
|
|
|
|
|
ids='delays={}'.format,
|
2019-10-30 04:16:39 +00:00
|
|
|
|
)
|
2026-04-22 01:33:15 +00:00
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
'num_subactors',
|
|
|
|
|
|
range(25, 26),
|
|
|
|
|
|
ids= 'num_subs={}'.format,
|
|
|
|
|
|
)
|
|
|
|
|
|
def test_multierror_fast_nursery(
|
|
|
|
|
|
reg_addr: tuple,
|
|
|
|
|
|
start_method: str,
|
|
|
|
|
|
num_subactors: int,
|
|
|
|
|
|
delay: float,
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
set_fork_aware_capture,
|
|
|
|
|
|
fail_after_w_trace: FailAfterWTraceFactory,
|
2026-04-22 01:33:15 +00:00
|
|
|
|
):
|
|
|
|
|
|
'''
|
|
|
|
|
|
Verify we raise a ``BaseExceptionGroup`` out of a nursery where
|
2019-10-30 04:16:39 +00:00
|
|
|
|
more then one actor errors and also with a delay before failure
|
|
|
|
|
|
to test failure during an ongoing spawning.
|
2026-04-22 01:33:15 +00:00
|
|
|
|
|
|
|
|
|
|
'''
|
2019-10-30 04:16:39 +00:00
|
|
|
|
async def main():
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
# budget = 2× natural trio-backend cascade time for
|
|
|
|
|
|
# 25 errorer subactors (~14s observed). on-timeout
|
|
|
|
|
|
# diag snapshot → if the cancel cascade hangs
|
|
|
|
|
|
# (observed under MTF backend with N>=14 errorer
|
|
|
|
|
|
# subactors) we get a fresh ptree/wchan/py-spy dump
|
|
|
|
|
|
# on disk INSTEAD of an opaque pytest timeout-kill.
|
|
|
|
|
|
# See `tractor/_testing/trace.py` for the helper.
|
|
|
|
|
|
async with fail_after_w_trace(30.0):
|
|
|
|
|
|
async with tractor.open_nursery(
|
|
|
|
|
|
registry_addrs=[reg_addr],
|
|
|
|
|
|
) as nursery:
|
2021-02-24 19:37:55 +00:00
|
|
|
|
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
for i in range(num_subactors):
|
|
|
|
|
|
await nursery.run_in_actor(
|
|
|
|
|
|
assert_err,
|
|
|
|
|
|
name=f'errorer{i}',
|
|
|
|
|
|
delay=delay
|
|
|
|
|
|
)
|
2019-10-30 04:16:39 +00:00
|
|
|
|
|
2022-10-09 17:41:08 +00:00
|
|
|
|
# with pytest.raises(trio.MultiError) as exc_info:
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
# NOTE, `trio.TooSlowError` from `fail_after_w_trace`
|
|
|
|
|
|
# bubbles UN-wrapped if `open_nursery.__aexit__` never
|
|
|
|
|
|
# gets re-entered; wrapped inside a `BaseExceptionGroup`
|
|
|
|
|
|
# if it did. Accept both shapes so the matcher itself
|
|
|
|
|
|
# doesn't lie about *what* failed.
|
|
|
|
|
|
with pytest.raises(
|
|
|
|
|
|
(BaseExceptionGroup, trio.TooSlowError),
|
|
|
|
|
|
) as exc_info:
|
2021-02-24 19:37:55 +00:00
|
|
|
|
trio.run(main)
|
2019-10-30 04:16:39 +00:00
|
|
|
|
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
if isinstance(exc_info.value, trio.TooSlowError):
|
|
|
|
|
|
pytest.fail(
|
|
|
|
|
|
f'cancel cascade hung past 12s '
|
|
|
|
|
|
f'(num_subactors={num_subactors}, delay={delay}); '
|
|
|
|
|
|
f'see stderr for `fail_after_w_trace` snapshot path'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2022-10-09 17:41:08 +00:00
|
|
|
|
assert exc_info.type == ExceptionGroup
|
2019-10-30 04:16:39 +00:00
|
|
|
|
err = exc_info.value
|
2021-07-02 15:55:16 +00:00
|
|
|
|
exceptions = err.exceptions
|
|
|
|
|
|
|
|
|
|
|
|
if len(exceptions) == 2:
|
|
|
|
|
|
# sometimes oddly now there's an embedded BrokenResourceError ?
|
2021-12-02 13:45:58 +00:00
|
|
|
|
for exc in exceptions:
|
|
|
|
|
|
excs = getattr(exc, 'exceptions', None)
|
|
|
|
|
|
if excs:
|
|
|
|
|
|
exceptions = excs
|
|
|
|
|
|
break
|
2021-07-02 15:55:16 +00:00
|
|
|
|
|
|
|
|
|
|
assert len(exceptions) == num_subactors
|
|
|
|
|
|
|
|
|
|
|
|
for exc in exceptions:
|
2019-10-30 04:16:39 +00:00
|
|
|
|
assert isinstance(exc, tractor.RemoteActorError)
|
2025-02-25 16:16:01 +00:00
|
|
|
|
assert exc.boxed_type is AssertionError
|
2019-10-30 04:16:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
2021-04-27 13:14:08 +00:00
|
|
|
|
async def do_nothing():
|
2018-09-01 18:52:48 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-22 01:33:15 +00:00
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
'mechanism', [
|
|
|
|
|
|
'nursery_cancel',
|
|
|
|
|
|
KeyboardInterrupt,
|
|
|
|
|
|
])
|
|
|
|
|
|
def test_cancel_single_subactor(
|
|
|
|
|
|
reg_addr: tuple,
|
|
|
|
|
|
mechanism: str|KeyboardInterrupt,
|
|
|
|
|
|
):
|
2025-03-20 21:50:22 +00:00
|
|
|
|
'''
|
|
|
|
|
|
Ensure a ``ActorNursery.start_actor()`` spawned subactor
|
2018-11-19 19:16:42 +00:00
|
|
|
|
cancels when the nursery is cancelled.
|
2025-03-20 21:50:22 +00:00
|
|
|
|
|
|
|
|
|
|
'''
|
2018-11-19 19:16:42 +00:00
|
|
|
|
async def spawn_actor():
|
2025-03-20 21:50:22 +00:00
|
|
|
|
'''
|
|
|
|
|
|
Spawn an actor that blocks indefinitely then cancel via
|
|
|
|
|
|
either `ActorNursery.cancel()` or an exception raise.
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
2021-02-24 19:37:55 +00:00
|
|
|
|
async with tractor.open_nursery(
|
2025-03-20 21:50:22 +00:00
|
|
|
|
registry_addrs=[reg_addr],
|
2021-02-24 19:37:55 +00:00
|
|
|
|
) as nursery:
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
|
|
|
|
|
portal = await nursery.start_actor(
|
2021-02-24 19:37:55 +00:00
|
|
|
|
'nothin', enable_modules=[__name__],
|
2018-09-01 18:52:48 +00:00
|
|
|
|
)
|
2020-12-22 15:35:05 +00:00
|
|
|
|
assert (await portal.run(do_nothing)) is None
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
2020-03-12 23:08:00 +00:00
|
|
|
|
if mechanism == 'nursery_cancel':
|
|
|
|
|
|
# would hang otherwise
|
|
|
|
|
|
await nursery.cancel()
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise mechanism
|
|
|
|
|
|
|
|
|
|
|
|
if mechanism == 'nursery_cancel':
|
2021-02-24 19:37:55 +00:00
|
|
|
|
trio.run(spawn_actor)
|
2020-03-12 23:08:00 +00:00
|
|
|
|
else:
|
|
|
|
|
|
with pytest.raises(mechanism):
|
2021-02-24 19:37:55 +00:00
|
|
|
|
trio.run(spawn_actor)
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def stream_forever():
|
|
|
|
|
|
for i in repeat("I can see these little future bubble things"):
|
|
|
|
|
|
# each yielded value is sent over the ``Channel`` to the
|
|
|
|
|
|
# parent actor
|
|
|
|
|
|
yield i
|
|
|
|
|
|
await trio.sleep(0.01)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-22 01:33:15 +00:00
|
|
|
|
@tractor_test(
|
|
|
|
|
|
timeout=6,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def test_cancel_infinite_streamer(
|
2026-04-23 18:37:48 +00:00
|
|
|
|
reg_addr: tuple,
|
|
|
|
|
|
start_method: str,
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
set_fork_aware_capture,
|
2026-04-22 01:33:15 +00:00
|
|
|
|
):
|
2018-09-01 18:52:48 +00:00
|
|
|
|
# stream for at most 1 seconds
|
2025-08-18 17:07:47 +00:00
|
|
|
|
with (
|
|
|
|
|
|
trio.fail_after(4),
|
|
|
|
|
|
trio.move_on_after(1) as cancel_scope
|
|
|
|
|
|
):
|
2018-09-01 18:52:48 +00:00
|
|
|
|
async with tractor.open_nursery() as n:
|
|
|
|
|
|
portal = await n.start_actor(
|
2020-07-21 04:23:14 +00:00
|
|
|
|
'donny',
|
2021-04-28 15:55:37 +00:00
|
|
|
|
enable_modules=[__name__],
|
2018-09-01 18:52:48 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# this async for loop streams values from the above
|
|
|
|
|
|
# async generator running in a separate process
|
2021-04-28 15:55:37 +00:00
|
|
|
|
async with portal.open_stream_from(stream_forever) as stream:
|
|
|
|
|
|
async for letter in stream:
|
|
|
|
|
|
print(letter)
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
|
|
|
|
|
# we support trio's cancellation system
|
|
|
|
|
|
assert cancel_scope.cancelled_caught
|
|
|
|
|
|
assert n.cancelled
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-11-19 19:16:42 +00:00
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
'num_actors_and_errs',
|
|
|
|
|
|
[
|
2019-10-25 20:43:53 +00:00
|
|
|
|
# daemon actors sit idle while single task actors error out
|
|
|
|
|
|
(1, tractor.RemoteActorError, AssertionError, (assert_err, {}), None),
|
2022-10-09 17:41:08 +00:00
|
|
|
|
(2, BaseExceptionGroup, AssertionError, (assert_err, {}), None),
|
|
|
|
|
|
(3, BaseExceptionGroup, AssertionError, (assert_err, {}), None),
|
2019-10-25 20:43:53 +00:00
|
|
|
|
|
|
|
|
|
|
# 1 daemon actor errors out while single task actors sleep forever
|
|
|
|
|
|
(3, tractor.RemoteActorError, AssertionError, (sleep_forever, {}),
|
|
|
|
|
|
(assert_err, {}, True)),
|
|
|
|
|
|
# daemon actors error out after brief delay while single task
|
|
|
|
|
|
# actors complete quickly
|
|
|
|
|
|
(3, tractor.RemoteActorError, AssertionError,
|
|
|
|
|
|
(do_nuthin, {}), (assert_err, {'delay': 1}, True)),
|
2019-10-26 19:04:13 +00:00
|
|
|
|
# daemon complete quickly delay while single task
|
|
|
|
|
|
# actors error after brief delay
|
2022-10-09 17:41:08 +00:00
|
|
|
|
(3, BaseExceptionGroup, AssertionError,
|
2019-10-25 20:43:53 +00:00
|
|
|
|
(assert_err, {'delay': 1}), (do_nuthin, {}, False)),
|
|
|
|
|
|
],
|
|
|
|
|
|
ids=[
|
|
|
|
|
|
'1_run_in_actor_fails',
|
|
|
|
|
|
'2_run_in_actors_fail',
|
|
|
|
|
|
'3_run_in_actors_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',
|
2018-11-19 19:16:42 +00:00
|
|
|
|
],
|
|
|
|
|
|
)
|
2026-04-22 01:33:15 +00:00
|
|
|
|
@tractor_test(
|
|
|
|
|
|
timeout=10,
|
|
|
|
|
|
)
|
2025-07-29 18:51:44 +00:00
|
|
|
|
async def test_some_cancels_all(
|
|
|
|
|
|
num_actors_and_errs: tuple,
|
2026-04-23 18:37:48 +00:00
|
|
|
|
reg_addr: tuple,
|
2025-07-29 18:51:44 +00:00
|
|
|
|
start_method: str,
|
|
|
|
|
|
loglevel: str,
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
set_fork_aware_capture, #: Callable,
|
2025-07-29 18:51:44 +00:00
|
|
|
|
):
|
|
|
|
|
|
'''
|
|
|
|
|
|
Verify a subset of failed subactors causes all others in
|
2018-11-22 16:43:04 +00:00
|
|
|
|
the nursery to be cancelled just like the strategy in trio.
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
|
|
|
|
|
This is the first and only supervisory strategy at the moment.
|
2025-07-29 18:51:44 +00:00
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
(
|
|
|
|
|
|
num_actors,
|
|
|
|
|
|
first_err,
|
|
|
|
|
|
err_type,
|
|
|
|
|
|
ria_func,
|
|
|
|
|
|
da_func,
|
|
|
|
|
|
) = num_actors_and_errs
|
2018-09-01 18:52:48 +00:00
|
|
|
|
try:
|
2025-07-29 18:51:44 +00:00
|
|
|
|
async with tractor.open_nursery() as an:
|
2019-10-25 20:43:53 +00:00
|
|
|
|
|
|
|
|
|
|
# spawn the same number of deamon actors which should be cancelled
|
|
|
|
|
|
dactor_portals = []
|
|
|
|
|
|
for i in range(num_actors):
|
2025-07-29 18:51:44 +00:00
|
|
|
|
dactor_portals.append(await an.start_actor(
|
2019-10-25 20:43:53 +00:00
|
|
|
|
f'deamon_{i}',
|
2021-02-24 19:37:55 +00:00
|
|
|
|
enable_modules=[__name__],
|
2018-09-01 18:52:48 +00:00
|
|
|
|
))
|
|
|
|
|
|
|
2019-10-25 20:43:53 +00:00
|
|
|
|
func, kwargs = ria_func
|
|
|
|
|
|
riactor_portals = []
|
|
|
|
|
|
for i in range(num_actors):
|
2018-11-22 16:43:04 +00:00
|
|
|
|
# start actor(s) that will fail immediately
|
2019-10-25 20:43:53 +00:00
|
|
|
|
riactor_portals.append(
|
2025-07-29 18:51:44 +00:00
|
|
|
|
await an.run_in_actor(
|
2020-12-21 14:09:55 +00:00
|
|
|
|
func,
|
|
|
|
|
|
name=f'actor_{i}',
|
|
|
|
|
|
**kwargs
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2019-10-25 20:43:53 +00:00
|
|
|
|
|
|
|
|
|
|
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:
|
2020-12-22 15:35:05 +00:00
|
|
|
|
await portal.run(func, **kwargs)
|
|
|
|
|
|
|
2019-10-25 20:43:53 +00:00
|
|
|
|
except tractor.RemoteActorError as err:
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert err.boxed_type == err_type
|
2019-10-25 20:43:53 +00:00
|
|
|
|
# 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?")
|
2018-11-19 19:16:42 +00:00
|
|
|
|
|
2018-11-22 16:43:04 +00:00
|
|
|
|
# should error here with a ``RemoteActorError`` or ``MultiError``
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
2025-07-29 18:51:44 +00:00
|
|
|
|
except first_err as _err:
|
|
|
|
|
|
err = _err
|
2022-10-09 17:41:08 +00:00
|
|
|
|
if isinstance(err, BaseExceptionGroup):
|
2019-10-25 20:43:53 +00:00
|
|
|
|
assert len(err.exceptions) == num_actors
|
2018-11-19 19:16:42 +00:00
|
|
|
|
for exc in err.exceptions:
|
2018-11-19 21:53:21 +00:00
|
|
|
|
if isinstance(exc, tractor.RemoteActorError):
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert exc.boxed_type == err_type
|
2018-11-19 21:53:21 +00:00
|
|
|
|
else:
|
|
|
|
|
|
assert isinstance(exc, trio.Cancelled)
|
|
|
|
|
|
elif isinstance(err, tractor.RemoteActorError):
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert err.boxed_type == err_type
|
2018-09-01 18:52:48 +00:00
|
|
|
|
|
2025-07-29 18:51:44 +00:00
|
|
|
|
assert an.cancelled is True
|
|
|
|
|
|
assert not an._children
|
2018-09-01 18:52:48 +00:00
|
|
|
|
else:
|
|
|
|
|
|
pytest.fail("Should have gotten a remote assertion error?")
|
2019-10-26 19:04:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-22 01:33:15 +00:00
|
|
|
|
async def spawn_and_error(
|
|
|
|
|
|
breadth: int,
|
|
|
|
|
|
depth: int,
|
|
|
|
|
|
) -> None:
|
2019-10-26 19:04:13 +00:00
|
|
|
|
name = tractor.current_actor().name
|
2019-10-30 04:16:39 +00:00
|
|
|
|
async with tractor.open_nursery() as nursery:
|
2019-11-25 00:22:01 +00:00
|
|
|
|
for i in range(breadth):
|
2020-12-21 14:09:55 +00:00
|
|
|
|
|
2019-11-25 00:22:01 +00:00
|
|
|
|
if depth > 0:
|
2020-12-21 14:09:55 +00:00
|
|
|
|
|
2019-11-25 00:22:01 +00:00
|
|
|
|
args = (
|
|
|
|
|
|
spawn_and_error,
|
|
|
|
|
|
)
|
|
|
|
|
|
kwargs = {
|
2020-12-21 14:09:55 +00:00
|
|
|
|
'name': f'spawner_{i}_depth_{depth}',
|
2019-11-25 00:22:01 +00:00
|
|
|
|
'breadth': breadth,
|
|
|
|
|
|
'depth': depth - 1,
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
args = (
|
|
|
|
|
|
assert_err,
|
|
|
|
|
|
)
|
2020-12-21 14:09:55 +00:00
|
|
|
|
kwargs = {
|
|
|
|
|
|
'name': f'{name}_errorer_{i}',
|
|
|
|
|
|
}
|
2019-11-25 00:22:01 +00:00
|
|
|
|
await nursery.run_in_actor(*args, **kwargs)
|
2019-10-26 19:04:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-27 23:55:37 +00:00
|
|
|
|
# NOTE: `main_thread_forkserver` capture-fd hang class is no
|
|
|
|
|
|
# longer skipped here — `--capture=sys` (the new `pyproject.toml`
|
|
|
|
|
|
# default) sidesteps the pipe-buffer-fill deadlock for
|
|
|
|
|
|
# `test_nested_multierrors`. See
|
|
|
|
|
|
# `ai/conc-anal/subint_forkserver_test_cancellation_leak_issue.md`
|
|
|
|
|
|
# / #449 for the post-mortem.
|
2026-04-29 22:10:40 +00:00
|
|
|
|
# @pytest.mark.timeout(
|
|
|
|
|
|
# 10,
|
|
|
|
|
|
# method='thread',
|
|
|
|
|
|
# )
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
'depth',
|
|
|
|
|
|
[1, 3],
|
|
|
|
|
|
ids='depth={}'.format,
|
|
|
|
|
|
)
|
|
|
|
|
|
@tractor_test(
|
|
|
|
|
|
# bumped from the 30s default to cover fork-based
|
|
|
|
|
|
# cancel-cascade flakes; 2 spawners × 2 errorers × depth 1+
|
|
|
|
|
|
# cascade through 6 portal-wait_for_result paths each
|
|
|
|
|
|
# paying `terminate_after=1.6s` + UDS sock-unlink under
|
|
|
|
|
|
# MTF/UDS contention can easily blow past 30s.
|
|
|
|
|
|
# Trio backend is fast and won't notice the extra budget.
|
|
|
|
|
|
# See `ai/conc-anal/cancel_cascade_too_slow_under_main_thread_forkserver_issue.md`.
|
|
|
|
|
|
timeout=10,
|
|
|
|
|
|
)
|
2026-04-22 01:33:15 +00:00
|
|
|
|
async def test_nested_multierrors(
|
2026-04-23 18:37:48 +00:00
|
|
|
|
reg_addr: tuple,
|
2026-04-22 01:33:15 +00:00
|
|
|
|
loglevel: str,
|
|
|
|
|
|
start_method: str,
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
set_fork_aware_capture,
|
|
|
|
|
|
fail_after_w_trace: FailAfterWTraceFactory,
|
|
|
|
|
|
request: pytest.FixtureRequest,
|
|
|
|
|
|
depth: int,
|
2026-04-22 01:33:15 +00:00
|
|
|
|
):
|
2022-01-20 13:26:30 +00:00
|
|
|
|
'''
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
Test that failed actor sets are wrapped in `BaseExceptionGroup`s.
|
|
|
|
|
|
|
|
|
|
|
|
Parametrized over recursion `depth ∈ {1, 3}`:
|
|
|
|
|
|
|
|
|
|
|
|
- `depth=1`: shallow tree (2 spawners × 2 errorers, 2
|
|
|
|
|
|
levels). Cascade completes well within budget on ALL
|
|
|
|
|
|
backends including MTF — regression-safety green case.
|
|
|
|
|
|
|
|
|
|
|
|
- `depth=3`: deep tree (2 spawners × recursive depth-3
|
|
|
|
|
|
spawn-and-error). On `main_thread_forkserver` this
|
|
|
|
|
|
trips the cancel-cascade shape-mismatch bug class
|
|
|
|
|
|
(see `ai/conc-anal/cancel_cascade_too_slow_under_main_thread_forkserver_issue.md`)
|
|
|
|
|
|
— xfailed below.
|
2022-01-20 13:26:30 +00:00
|
|
|
|
|
|
|
|
|
|
'''
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
# XXX: `multiprocessing.forkserver` can't handle nested
|
|
|
|
|
|
# spawning at any depth — hangs / broken-pipes. Pre-existing
|
|
|
|
|
|
# backend limitation, NOT depth-specific.
|
|
|
|
|
|
if start_method == 'forkserver':
|
|
|
|
|
|
pytest.skip("Forksever sux hard at nested spawning...")
|
|
|
|
|
|
|
|
|
|
|
|
subactor_breadth = 2
|
|
|
|
|
|
|
|
|
|
|
|
# MTF backend trips a probabilistic timing race in the
|
|
|
|
|
|
# cancel-cascade — NOT depth-gated; depth amplifies the
|
|
|
|
|
|
# variance so depth=3 misses nearly every run while
|
|
|
|
|
|
# depth=1 misses occasionally. Both get the xfail mark
|
|
|
|
|
|
# (with `strict=False`) since the bug class can fire at
|
|
|
|
|
|
# either depth.
|
|
|
|
|
|
#
|
|
|
|
|
|
# The scenario in detail:
|
|
|
|
|
|
#
|
|
|
|
|
|
# T=0 spawn spawner_0 + spawner_1 in parallel
|
|
|
|
|
|
# T=t1 spawner_0's child errors →
|
|
|
|
|
|
# RemoteActorError reaches root nursery
|
|
|
|
|
|
# T=t1+ε root nursery starts cancelling
|
|
|
|
|
|
# spawner_1's portal-wait
|
|
|
|
|
|
# T=t2 spawner_1's child errors → tries to send
|
|
|
|
|
|
# RemoteActorError back
|
|
|
|
|
|
#
|
|
|
|
|
|
# if t2 < t1+ε: BEG = [RAE, RAE] ← clean (xpass)
|
|
|
|
|
|
# if t2 > t1+ε: BEG = [RAE, Cancelled] ← race tripped (xfail)
|
|
|
|
|
|
#
|
|
|
|
|
|
# i.e. the assertion below (`isinstance(_, RemoteActorError)`)
|
|
|
|
|
|
# fails iff cancel-delivery beats the other tree's natural
|
|
|
|
|
|
# error-propagation. Depth amplifies `t2-t1` variance
|
|
|
|
|
|
# (longer per-tree paths = more skew); under MTF the
|
|
|
|
|
|
# fork-spawn jitter + UDS-contention widens both `t1` and
|
|
|
|
|
|
# `t2` further.
|
|
|
|
|
|
#
|
|
|
|
|
|
# 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` —
|
|
|
|
|
|
# neither flakes `--lf`. When MTF cancel-cascade
|
|
|
|
|
|
# eventually speeds up enough to close the race even at
|
|
|
|
|
|
# depth=3, BOTH variants will reliably `xpass` and
|
|
|
|
|
|
# pytest will yell — our signal to drop the marker. See
|
|
|
|
|
|
# `ai/conc-anal/cancel_cascade_too_slow_under_main_thread_forkserver_issue.md`.
|
|
|
|
|
|
if start_method == 'main_thread_forkserver':
|
|
|
|
|
|
request.node.add_marker(
|
|
|
|
|
|
pytest.mark.xfail(
|
|
|
|
|
|
strict=False,
|
|
|
|
|
|
reason=(
|
|
|
|
|
|
f'MTF cancel-cascade shape-mismatch at '
|
|
|
|
|
|
f'depth={depth} (Cancelled races '
|
|
|
|
|
|
f'RemoteActorError in BEG); see conc-anal/'
|
|
|
|
|
|
'cancel_cascade_too_slow_under_main_thread_forkserver_issue.md'
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 6s budget: in the non-hang case (and on the trio
|
|
|
|
|
|
# backend) the whole spawn + cancel-cascade should
|
|
|
|
|
|
# complete in well under that. On the borderline hang
|
|
|
|
|
|
# case the `fail_after_w_trace` fires `TooSlowError`
|
|
|
|
|
|
# AND captures a ptree/wchan/py-spy snapshot to
|
|
|
|
|
|
# `$XDG_CACHE_HOME/tractor/hung-dumps/` for offline
|
|
|
|
|
|
# inspection. See
|
|
|
|
|
|
# `ai/conc-anal/cancel_cascade_too_slow_under_main_thread_forkserver_issue.md`.
|
|
|
|
|
|
match (start_method, depth):
|
|
|
|
|
|
case ('trio', _):
|
|
|
|
|
|
timeout = 6
|
|
|
|
|
|
case ('main_thread_forkserver', 1):
|
|
|
|
|
|
timeout = 16
|
|
|
|
|
|
case ('main_thread_forkserver', 3):
|
|
|
|
|
|
timeout = 30
|
|
|
|
|
|
|
|
|
|
|
|
async with fail_after_w_trace(timeout):
|
2019-11-25 00:22:01 +00:00
|
|
|
|
try:
|
|
|
|
|
|
async with tractor.open_nursery() as nursery:
|
|
|
|
|
|
for i in range(subactor_breadth):
|
|
|
|
|
|
await nursery.run_in_actor(
|
|
|
|
|
|
spawn_and_error,
|
2020-12-21 14:09:55 +00:00
|
|
|
|
name=f'spawner_{i}',
|
2019-11-25 00:22:01 +00:00
|
|
|
|
breadth=subactor_breadth,
|
|
|
|
|
|
depth=depth,
|
|
|
|
|
|
)
|
2022-10-09 17:41:08 +00:00
|
|
|
|
except BaseExceptionGroup as err:
|
2019-11-25 00:22:01 +00:00
|
|
|
|
assert len(err.exceptions) == subactor_breadth
|
|
|
|
|
|
for subexc in err.exceptions:
|
|
|
|
|
|
|
2020-07-26 01:20:34 +00:00
|
|
|
|
# verify first level actor errors are wrapped as remote
|
2026-03-06 17:03:33 +00:00
|
|
|
|
if _friggin_windows:
|
2020-07-26 01:20:34 +00:00
|
|
|
|
|
|
|
|
|
|
# windows is often too slow and cancellation seems
|
|
|
|
|
|
# to happen before an actor is spawned
|
2020-07-27 15:14:21 +00:00
|
|
|
|
if isinstance(subexc, trio.Cancelled):
|
2020-07-26 01:20:34 +00:00
|
|
|
|
continue
|
2021-10-14 16:12:13 +00:00
|
|
|
|
|
|
|
|
|
|
elif isinstance(subexc, tractor.RemoteActorError):
|
2020-07-27 15:14:21 +00:00
|
|
|
|
# on windows it seems we can't exactly be sure wtf
|
|
|
|
|
|
# will happen..
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert subexc.boxed_type in (
|
2020-07-27 15:14:21 +00:00
|
|
|
|
tractor.RemoteActorError,
|
|
|
|
|
|
trio.Cancelled,
|
2022-10-09 17:41:08 +00:00
|
|
|
|
BaseExceptionGroup,
|
2020-07-27 15:14:21 +00:00
|
|
|
|
)
|
2021-10-14 16:12:13 +00:00
|
|
|
|
|
2022-10-09 17:41:08 +00:00
|
|
|
|
elif isinstance(subexc, BaseExceptionGroup):
|
2021-10-14 16:12:13 +00:00
|
|
|
|
for subsub in subexc.exceptions:
|
2021-10-15 13:16:51 +00:00
|
|
|
|
|
|
|
|
|
|
if subsub in (tractor.RemoteActorError,):
|
2024-03-19 22:08:54 +00:00
|
|
|
|
subsub = subsub.boxed_type
|
2021-10-15 13:16:51 +00:00
|
|
|
|
|
2021-10-15 14:07:45 +00:00
|
|
|
|
assert type(subsub) in (
|
2021-10-14 16:12:13 +00:00
|
|
|
|
trio.Cancelled,
|
2022-10-09 17:41:08 +00:00
|
|
|
|
BaseExceptionGroup,
|
2021-10-14 16:12:13 +00:00
|
|
|
|
)
|
2020-07-26 01:20:34 +00:00
|
|
|
|
else:
|
|
|
|
|
|
assert isinstance(subexc, tractor.RemoteActorError)
|
|
|
|
|
|
|
|
|
|
|
|
if depth > 0 and subactor_breadth > 1:
|
2019-11-25 00:22:01 +00:00
|
|
|
|
# XXX not sure what's up with this..
|
2020-07-26 01:20:34 +00:00
|
|
|
|
# on windows sometimes spawning is just too slow and
|
|
|
|
|
|
# we get back the (sent) cancel signal instead
|
2026-03-06 17:03:33 +00:00
|
|
|
|
if _friggin_windows:
|
2021-10-15 14:07:45 +00:00
|
|
|
|
if isinstance(subexc, tractor.RemoteActorError):
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert subexc.boxed_type in (
|
2022-10-09 17:41:08 +00:00
|
|
|
|
BaseExceptionGroup,
|
2022-01-20 13:26:30 +00:00
|
|
|
|
tractor.RemoteActorError
|
|
|
|
|
|
)
|
2021-10-15 14:07:45 +00:00
|
|
|
|
else:
|
2022-10-09 17:41:08 +00:00
|
|
|
|
assert isinstance(subexc, BaseExceptionGroup)
|
2019-11-25 00:22:01 +00:00
|
|
|
|
else:
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert subexc.boxed_type is ExceptionGroup
|
2019-11-25 00:22:01 +00:00
|
|
|
|
else:
|
2024-03-19 22:08:54 +00:00
|
|
|
|
assert subexc.boxed_type in (
|
2022-01-20 13:26:30 +00:00
|
|
|
|
tractor.RemoteActorError,
|
|
|
|
|
|
trio.Cancelled
|
|
|
|
|
|
)
|
2020-07-21 04:23:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
2020-07-25 16:00:04 +00:00
|
|
|
|
@no_windows
|
2020-07-29 17:27:15 +00:00
|
|
|
|
def test_cancel_via_SIGINT(
|
2026-04-23 18:37:48 +00:00
|
|
|
|
reg_addr: tuple,
|
2026-04-22 01:33:15 +00:00
|
|
|
|
loglevel: str,
|
|
|
|
|
|
start_method: str,
|
2020-07-29 17:27:15 +00:00
|
|
|
|
):
|
2026-04-22 01:33:15 +00:00
|
|
|
|
'''
|
|
|
|
|
|
Ensure that a control-C (SIGINT) signal cancels both the parent and
|
2020-07-21 04:23:14 +00:00
|
|
|
|
child processes in trionic fashion
|
2026-04-22 01:33:15 +00:00
|
|
|
|
|
|
|
|
|
|
'''
|
2026-03-25 20:50:55 +00:00
|
|
|
|
pid: int = os.getpid()
|
2020-07-21 04:23:14 +00:00
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
|
with trio.fail_after(2):
|
2026-04-23 18:37:48 +00:00
|
|
|
|
async with tractor.open_nursery(
|
|
|
|
|
|
registry_addrs=[reg_addr],
|
|
|
|
|
|
) as tn:
|
2020-07-21 04:23:14 +00:00
|
|
|
|
await tn.start_actor('sucka')
|
2026-04-22 01:33:15 +00:00
|
|
|
|
if 'mp' in start_method:
|
2020-07-29 17:27:15 +00:00
|
|
|
|
time.sleep(0.1)
|
2020-07-21 04:23:14 +00:00
|
|
|
|
os.kill(pid, signal.SIGINT)
|
|
|
|
|
|
await trio.sleep_forever()
|
|
|
|
|
|
|
|
|
|
|
|
with pytest.raises(KeyboardInterrupt):
|
2021-02-24 19:37:55 +00:00
|
|
|
|
trio.run(main)
|
2020-07-21 04:23:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
2020-07-25 16:00:04 +00:00
|
|
|
|
@no_windows
|
2020-07-27 03:37:44 +00:00
|
|
|
|
def test_cancel_via_SIGINT_other_task(
|
2026-04-23 18:37:48 +00:00
|
|
|
|
reg_addr: tuple,
|
2026-03-06 17:03:33 +00:00
|
|
|
|
loglevel: str,
|
|
|
|
|
|
start_method: str,
|
|
|
|
|
|
spawn_backend: str,
|
2020-07-21 04:23:14 +00:00
|
|
|
|
):
|
2026-03-06 17:03:33 +00:00
|
|
|
|
'''
|
|
|
|
|
|
Ensure that a control-C (SIGINT) signal cancels both the parent
|
|
|
|
|
|
and child processes in trionic fashion even a subprocess is
|
|
|
|
|
|
started from a seperate ``trio`` child task.
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
2026-03-25 20:50:55 +00:00
|
|
|
|
from .conftest import cpu_scaling_factor
|
|
|
|
|
|
|
2026-03-06 17:03:33 +00:00
|
|
|
|
pid: int = os.getpid()
|
|
|
|
|
|
timeout: float = (
|
|
|
|
|
|
4 if _non_linux
|
|
|
|
|
|
else 2
|
|
|
|
|
|
)
|
|
|
|
|
|
if _friggin_windows: # smh
|
2022-01-20 13:26:30 +00:00
|
|
|
|
timeout += 1
|
2020-07-21 04:23:14 +00:00
|
|
|
|
|
2026-03-25 20:50:55 +00:00
|
|
|
|
# add latency headroom for CPU freq scaling (auto-cpufreq et al.)
|
|
|
|
|
|
headroom: float = cpu_scaling_factor()
|
|
|
|
|
|
if headroom != 1.:
|
|
|
|
|
|
timeout *= headroom
|
|
|
|
|
|
|
2025-02-25 16:16:01 +00:00
|
|
|
|
async def spawn_and_sleep_forever(
|
|
|
|
|
|
task_status=trio.TASK_STATUS_IGNORED
|
|
|
|
|
|
):
|
2026-04-23 18:37:48 +00:00
|
|
|
|
async with tractor.open_nursery(
|
|
|
|
|
|
registry_addrs=[reg_addr],
|
|
|
|
|
|
) as tn:
|
2020-07-21 04:23:14 +00:00
|
|
|
|
for i in range(3):
|
2020-12-21 14:09:55 +00:00
|
|
|
|
await tn.run_in_actor(
|
|
|
|
|
|
sleep_forever,
|
|
|
|
|
|
name='namesucka',
|
|
|
|
|
|
)
|
2020-07-21 04:23:14 +00:00
|
|
|
|
task_status.started()
|
|
|
|
|
|
await trio.sleep_forever()
|
|
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
|
# should never timeout since SIGINT should cancel the current program
|
2022-01-20 13:26:30 +00:00
|
|
|
|
with trio.fail_after(timeout):
|
2025-08-18 16:03:32 +00:00
|
|
|
|
async with (
|
|
|
|
|
|
|
|
|
|
|
|
# XXX ?TODO? why no work!?
|
|
|
|
|
|
# tractor.trionics.collapse_eg(),
|
|
|
|
|
|
trio.open_nursery(
|
|
|
|
|
|
strict_exception_groups=False,
|
|
|
|
|
|
) as tn,
|
|
|
|
|
|
):
|
|
|
|
|
|
await tn.start(spawn_and_sleep_forever)
|
2022-10-09 21:54:55 +00:00
|
|
|
|
if 'mp' in spawn_backend:
|
2020-07-29 17:27:15 +00:00
|
|
|
|
time.sleep(0.1)
|
2020-07-21 04:23:14 +00:00
|
|
|
|
os.kill(pid, signal.SIGINT)
|
|
|
|
|
|
|
|
|
|
|
|
with pytest.raises(KeyboardInterrupt):
|
2021-02-24 19:37:55 +00:00
|
|
|
|
trio.run(main)
|
2020-10-12 12:56:49 +00:00
|
|
|
|
|
2021-10-14 16:12:13 +00:00
|
|
|
|
|
2020-10-12 12:56:49 +00:00
|
|
|
|
async def spin_for(period=3):
|
|
|
|
|
|
"Sync sleep."
|
2025-08-10 17:57:04 +00:00
|
|
|
|
print(f'sync sleeping in sub-sub for {period}\n')
|
2020-10-12 12:56:49 +00:00
|
|
|
|
time.sleep(period)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-10 17:57:04 +00:00
|
|
|
|
async def spawn_sub_with_sync_blocking_task():
|
|
|
|
|
|
async with tractor.open_nursery() as an:
|
|
|
|
|
|
print('starting sync blocking subactor..\n')
|
|
|
|
|
|
await an.run_in_actor(
|
2020-12-21 14:09:55 +00:00
|
|
|
|
spin_for,
|
|
|
|
|
|
name='sleeper',
|
|
|
|
|
|
)
|
2025-08-10 17:57:04 +00:00
|
|
|
|
print('exiting first subactor layer..\n')
|
2020-10-12 12:56:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
2025-08-10 17:57:04 +00:00
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
|
'man_cancel_outer',
|
|
|
|
|
|
[
|
|
|
|
|
|
False, # passes if delay != 2
|
|
|
|
|
|
|
|
|
|
|
|
# always causes an unexpected eg-w-embedded-assert-err?
|
|
|
|
|
|
pytest.param(True,
|
|
|
|
|
|
marks=pytest.mark.xfail(
|
|
|
|
|
|
reason=(
|
|
|
|
|
|
'always causes an unexpected eg-w-embedded-assert-err?'
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
2020-10-13 19:26:14 +00:00
|
|
|
|
@no_windows
|
2020-10-12 12:56:49 +00:00
|
|
|
|
def test_cancel_while_childs_child_in_sync_sleep(
|
2025-08-10 17:57:04 +00:00
|
|
|
|
loglevel: str,
|
|
|
|
|
|
start_method: str,
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
is_forking_spawner: bool,
|
2025-08-10 17:57:04 +00:00
|
|
|
|
debug_mode: bool,
|
|
|
|
|
|
reg_addr: tuple,
|
|
|
|
|
|
man_cancel_outer: bool,
|
2020-10-12 12:56:49 +00:00
|
|
|
|
):
|
2025-08-10 17:57:04 +00:00
|
|
|
|
'''
|
|
|
|
|
|
Verify that a child cancelled while executing sync code is torn
|
2020-10-12 12:56:49 +00:00
|
|
|
|
down even when that cancellation is triggered by the parent
|
|
|
|
|
|
2 nurseries "up".
|
2025-08-10 17:57:04 +00:00
|
|
|
|
|
|
|
|
|
|
Though the grandchild should stay blocking its actor runtime, its
|
|
|
|
|
|
parent should issue a "zombie reaper" to hard kill it after
|
|
|
|
|
|
sufficient timeout.
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
2020-10-13 18:16:20 +00:00
|
|
|
|
if start_method == 'forkserver':
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
pytest.skip(
|
|
|
|
|
|
"`multiprocessing`'s forkserver sux hard at "
|
|
|
|
|
|
"resuming from sync sleep..."
|
|
|
|
|
|
)
|
2020-10-13 18:16:20 +00:00
|
|
|
|
|
2020-10-12 12:56:49 +00:00
|
|
|
|
async def main():
|
2025-08-10 17:57:04 +00:00
|
|
|
|
#
|
|
|
|
|
|
# XXX BIG TODO NOTE XXX
|
|
|
|
|
|
#
|
|
|
|
|
|
# it seems there's a strange race that can happen
|
|
|
|
|
|
# where where the fail-after will trigger outer scope
|
|
|
|
|
|
# .cancel() which then causes the inner scope to raise,
|
|
|
|
|
|
#
|
|
|
|
|
|
# BaseExceptionGroup('Exceptions from Trio nursery', [
|
|
|
|
|
|
# BaseExceptionGroup('Exceptions from Trio nursery',
|
|
|
|
|
|
# [
|
|
|
|
|
|
# Cancelled(),
|
|
|
|
|
|
# Cancelled(),
|
|
|
|
|
|
# ]
|
|
|
|
|
|
# ),
|
|
|
|
|
|
# AssertionError('assert 0')
|
|
|
|
|
|
# ])
|
|
|
|
|
|
#
|
|
|
|
|
|
# WHY THIS DOESN'T MAKE SENSE:
|
|
|
|
|
|
# ---------------------------
|
|
|
|
|
|
# - it should raise too-slow-error when too slow..
|
|
|
|
|
|
# * verified that using simple-cs and manually cancelling
|
|
|
|
|
|
# you get same outcome -> indicates that the fail-after
|
|
|
|
|
|
# can have its TooSlowError overriden!
|
|
|
|
|
|
# |_ to check this it's easy, simplly decrease the timeout
|
|
|
|
|
|
# as per the var below.
|
|
|
|
|
|
#
|
|
|
|
|
|
# - when using the manual simple-cs the outcome is different
|
|
|
|
|
|
# DESPITE the `assert 0` which means regardless of the
|
|
|
|
|
|
# inner scope effectively failing in the same way, the
|
|
|
|
|
|
# bubbling up **is NOT the same**.
|
|
|
|
|
|
#
|
|
|
|
|
|
# delays trigger diff outcomes..
|
|
|
|
|
|
# ---------------------------
|
|
|
|
|
|
# as seen by uncommenting various lines below there is from
|
|
|
|
|
|
# my POV an unexpected outcome due to the delay=2 case.
|
|
|
|
|
|
#
|
|
|
|
|
|
# delay = 1 # no AssertionError in eg, TooSlowError raised.
|
|
|
|
|
|
# delay = 2 # is AssertionError in eg AND no TooSlowError !?
|
2026-03-13 22:47:58 +00:00
|
|
|
|
# is AssertionError in eg AND no _cs cancellation.
|
|
|
|
|
|
delay = (
|
Harden `test_cancellation` for fork-spawner backends
Deats,
- `pytestmark`: enrich `skipon_spawn_backend('subint')` reason with
conc-anal doc refs + GH#379 link, add `reap_subactors_per_test`,
`track_orphaned_uds_per_test`,
`detect_runaway_subactors_per_test` fixtures
- `test_nested_multierrors`: parametrize over `depth` `{1, 3}`, add
MTF `xfail(strict=False)` with detailed race-window comment
explaining the BEG shape mismatch, wrap body in
`fail_after_w_trace` with per-backend timeout budget, bump
`@tractor_test(timeout=10)`, drop old multiprocessing depth
special-casing
- `test_multierror_fast_nursery`: wrap in
`fail_after_w_trace(30.0)`, accept `TooSlowError` in
`pytest.raises`, surface explicit `pytest.fail` on hang
- `test_cancel_while_childs_child_in_sync_sleep`: swap
`spawn_backend` param for `is_forking_spawner`, widen
`fail_after` delay for fork-based spawners
- `test_remote_error`, `test_multierror`,
`test_cancel_infinite_streamer`, `test_some_cancels_all`: add
`set_fork_aware_capture` fixture param
- Drop commented-out per-test `skipon_spawn_backend` blocks (now
covered by module-level `pytestmark`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-05-14 00:10:02 +00:00
|
|
|
|
6 if (
|
|
|
|
|
|
_non_linux
|
|
|
|
|
|
or
|
|
|
|
|
|
is_forking_spawner
|
|
|
|
|
|
)
|
2026-03-13 22:47:58 +00:00
|
|
|
|
else 4
|
|
|
|
|
|
)
|
2025-08-10 17:57:04 +00:00
|
|
|
|
|
|
|
|
|
|
with trio.fail_after(delay) as _cs:
|
|
|
|
|
|
# with trio.CancelScope() as cs:
|
|
|
|
|
|
# ^XXX^ can be used instead to see same outcome.
|
|
|
|
|
|
|
2025-07-29 18:51:44 +00:00
|
|
|
|
async with (
|
2025-08-10 17:57:04 +00:00
|
|
|
|
# tractor.trionics.collapse_eg(), # doesn't help
|
|
|
|
|
|
tractor.open_nursery(
|
|
|
|
|
|
hide_tb=False,
|
|
|
|
|
|
debug_mode=debug_mode,
|
|
|
|
|
|
registry_addrs=[reg_addr],
|
|
|
|
|
|
) as an,
|
2025-07-29 18:51:44 +00:00
|
|
|
|
):
|
|
|
|
|
|
await an.run_in_actor(
|
2025-08-10 17:57:04 +00:00
|
|
|
|
spawn_sub_with_sync_blocking_task,
|
|
|
|
|
|
name='sync_blocking_sub',
|
2020-12-21 14:09:55 +00:00
|
|
|
|
)
|
2020-10-12 12:56:49 +00:00
|
|
|
|
await trio.sleep(1)
|
2025-08-10 17:57:04 +00:00
|
|
|
|
|
|
|
|
|
|
if man_cancel_outer:
|
|
|
|
|
|
print('Cancelling manually in root')
|
|
|
|
|
|
_cs.cancel()
|
|
|
|
|
|
|
|
|
|
|
|
# trigger exc-srced taskc down
|
|
|
|
|
|
# the actor tree.
|
|
|
|
|
|
print('RAISING IN ROOT')
|
2020-10-12 12:56:49 +00:00
|
|
|
|
assert 0
|
|
|
|
|
|
|
|
|
|
|
|
with pytest.raises(AssertionError):
|
2021-02-24 19:37:55 +00:00
|
|
|
|
trio.run(main)
|
2021-11-29 17:41:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_fast_graceful_cancel_when_spawn_task_in_soft_proc_wait_for_daemon(
|
2026-04-22 01:33:15 +00:00
|
|
|
|
start_method: str,
|
2021-11-29 17:41:40 +00:00
|
|
|
|
):
|
|
|
|
|
|
'''
|
|
|
|
|
|
This is a very subtle test which demonstrates how cancellation
|
|
|
|
|
|
during process collection can result in non-optimal teardown
|
|
|
|
|
|
performance on daemon actors. The fix for this test was to handle
|
|
|
|
|
|
``trio.Cancelled`` specially in the spawn task waiting in
|
|
|
|
|
|
`proc.wait()` such that ``Portal.cancel_actor()`` is called before
|
|
|
|
|
|
executing the "hard reap" sequence (which has an up to 3 second
|
|
|
|
|
|
delay currently).
|
|
|
|
|
|
|
|
|
|
|
|
In other words, if we can cancel the actor using a graceful remote
|
|
|
|
|
|
cancellation, and it's faster, we might as well do it.
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
2021-12-02 13:12:46 +00:00
|
|
|
|
kbi_delay = 0.5
|
2022-01-20 13:26:30 +00:00
|
|
|
|
timeout: float = 2.9
|
|
|
|
|
|
|
2026-03-06 17:03:33 +00:00
|
|
|
|
if _friggin_windows: # smh
|
2022-01-20 13:26:30 +00:00
|
|
|
|
timeout += 1
|
2021-11-29 17:41:40 +00:00
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
|
start = time.time()
|
|
|
|
|
|
try:
|
|
|
|
|
|
async with trio.open_nursery() as nurse:
|
|
|
|
|
|
async with tractor.open_nursery() as tn:
|
|
|
|
|
|
p = await tn.start_actor(
|
|
|
|
|
|
'fast_boi',
|
|
|
|
|
|
enable_modules=[__name__],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def delayed_kbi():
|
|
|
|
|
|
await trio.sleep(kbi_delay)
|
|
|
|
|
|
print(f'RAISING KBI after {kbi_delay} s')
|
|
|
|
|
|
raise KeyboardInterrupt
|
|
|
|
|
|
|
|
|
|
|
|
# start task which raises a kbi **after**
|
|
|
|
|
|
# the actor nursery ``__aexit__()`` has
|
|
|
|
|
|
# been run.
|
|
|
|
|
|
nurse.start_soon(delayed_kbi)
|
|
|
|
|
|
|
|
|
|
|
|
await p.run(do_nuthin)
|
2025-03-03 17:24:29 +00:00
|
|
|
|
|
|
|
|
|
|
# need to explicitly re-raise the lone kbi..now
|
|
|
|
|
|
except* KeyboardInterrupt as kbi_eg:
|
|
|
|
|
|
assert (len(excs := kbi_eg.exceptions) == 1)
|
|
|
|
|
|
raise excs[0]
|
|
|
|
|
|
|
2021-11-29 17:41:40 +00:00
|
|
|
|
finally:
|
|
|
|
|
|
duration = time.time() - start
|
2022-01-20 13:26:30 +00:00
|
|
|
|
if duration > timeout:
|
2021-11-29 17:41:40 +00:00
|
|
|
|
raise trio.TooSlowError(
|
|
|
|
|
|
'daemon cancel was slower then necessary..'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
with pytest.raises(KeyboardInterrupt):
|
|
|
|
|
|
trio.run(main)
|