Mark `subint`-hanging tests with `skipon_spawn_backend`

Adopt the `@pytest.mark.skipon_spawn_backend('subint',
reason=...)` marker (a617b521) across the suites
reproducing the `subint` GIL-contention / starvation
hang classes doc'd in `ai/conc-anal/subint_*_issue.md`.

Deats,
- Module-level `pytestmark` on full-file-hanging suites:
  - `tests/test_cancellation.py`
  - `tests/test_inter_peer_cancellation.py`
  - `tests/test_pubsub.py`
  - `tests/test_shm.py`
- Per-test decorator where only one test in the file
  hangs:
  - `tests/discovery/test_registrar.py
    ::test_stale_entry_is_deleted` — replaces the
    inline `if start_method == 'subint': pytest.skip`
    branch with a declarative skip.
  - `tests/test_subint_cancellation.py
    ::test_subint_non_checkpointing_child`.
- A few per-test decorators are left commented-in-
  place as breadcrumbs for later finer-grained unskips.

Also, some nearby tidying in the affected files:
- Annotate loose fixture / test params
  (`pytest.FixtureRequest`, `str`, `tuple`, `bool`) in
  `tests/conftest.py`, `tests/devx/conftest.py`, and
  `tests/test_cancellation.py`.
- Normalize `"""..."""` → `'''...'''` docstrings per
  repo convention on a few touched tests.
- Add `timeout=6` / `timeout=10` to
  `@tractor_test(...)` on `test_cancel_infinite_streamer`
  and `test_some_cancels_all`.
- Drop redundant `spawn_backend` param from
  `test_cancel_via_SIGINT`; use `start_method` in the
  `'mp' in ...` check instead.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
subint_spawner_backend
Gud Boi 2026-04-21 21:33:15 -04:00
parent a617b52140
commit 99d70337b7
8 changed files with 136 additions and 33 deletions

View File

@ -139,7 +139,9 @@ def pytest_addoption(
@pytest.fixture(scope='session', autouse=True) @pytest.fixture(scope='session', autouse=True)
def loglevel(request) -> str: def loglevel(
request: pytest.FixtureRequest,
) -> str:
import tractor import tractor
orig = tractor.log._default_loglevel orig = tractor.log._default_loglevel
level = tractor.log._default_loglevel = request.config.option.loglevel level = tractor.log._default_loglevel = request.config.option.loglevel
@ -156,7 +158,7 @@ def loglevel(request) -> str:
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def test_log( def test_log(
request, request: pytest.FixtureRequest,
loglevel: str, loglevel: str,
) -> tractor.log.StackLevelAdapter: ) -> tractor.log.StackLevelAdapter:
''' '''

View File

@ -146,13 +146,12 @@ def spawn(
ids='ctl-c={}'.format, ids='ctl-c={}'.format,
) )
def ctlc( def ctlc(
request, request: pytest.FixtureRequest,
ci_env: bool, ci_env: bool,
) -> bool: ) -> bool:
use_ctlc = request.param use_ctlc: bool = request.param
node = request.node node = request.node
markers = node.own_markers markers = node.own_markers
for mark in markers: for mark in markers:

View File

@ -520,8 +520,6 @@ async def kill_transport(
# @pytest.mark.parametrize('use_signal', [False, True])
#
# Wall-clock bound via `pytest-timeout` (`method='thread'`). # Wall-clock bound via `pytest-timeout` (`method='thread'`).
# Under `--spawn-backend=subint` this test can wedge in an # Under `--spawn-backend=subint` this test can wedge in an
# un-Ctrl-C-able state (abandoned-subint + shared-GIL # un-Ctrl-C-able state (abandoned-subint + shared-GIL
@ -537,6 +535,16 @@ async def kill_transport(
3, # NOTE should be a 2.1s happy path. 3, # NOTE should be a 2.1s happy path.
method='thread', method='thread',
) )
@pytest.mark.skipon_spawn_backend(
'subint',
reason=(
'XXX SUBINT HANGING TEST XXX\n'
'See oustanding issue(s)\n'
# TODO, put issue link!
)
)
# @pytest.mark.parametrize('use_signal', [False, True])
#
def test_stale_entry_is_deleted( def test_stale_entry_is_deleted(
debug_mode: bool, debug_mode: bool,
daemon: subprocess.Popen, daemon: subprocess.Popen,
@ -549,12 +557,6 @@ def test_stale_entry_is_deleted(
stale entry and not delivering a bad portal. stale entry and not delivering a bad portal.
''' '''
if start_method == 'subint':
pytest.skip(
'XXX SUBINT HANGING TEST XXX\n'
'See oustanding issue(s)\n'
)
async def main(): async def main():
name: str = 'transport_fails_actor' name: str = 'transport_fails_actor'

View File

@ -21,6 +21,16 @@ _non_linux: bool = platform.system() != 'Linux'
_friggin_windows: bool = platform.system() == 'Windows' _friggin_windows: bool = platform.system() == 'Windows'
pytestmark = pytest.mark.skipon_spawn_backend(
'subint',
reason=(
'XXX SUBINT HANGING TEST XXX\n'
'See oustanding issue(s)\n'
# TODO, put issue link!
)
)
async def assert_err(delay=0): async def assert_err(delay=0):
await trio.sleep(delay) await trio.sleep(delay)
assert 0 assert 0
@ -110,8 +120,17 @@ def test_remote_error(reg_addr, args_err):
assert exc.boxed_type == errtype assert exc.boxed_type == errtype
# @pytest.mark.skipon_spawn_backend(
# 'subint',
# reason=(
# 'XXX SUBINT HANGING TEST XXX\n'
# 'See oustanding issue(s)\n'
# # TODO, put issue link!
# )
# )
def test_multierror( def test_multierror(
reg_addr: tuple[str, int], reg_addr: tuple[str, int],
start_method: str,
): ):
''' '''
Verify we raise a ``BaseExceptionGroup`` out of a nursery where Verify we raise a ``BaseExceptionGroup`` out of a nursery where
@ -141,15 +160,28 @@ def test_multierror(
trio.run(main) trio.run(main)
@pytest.mark.parametrize('delay', (0, 0.5))
@pytest.mark.parametrize( @pytest.mark.parametrize(
'num_subactors', range(25, 26), 'delay',
(0, 0.5),
ids='delays={}'.format,
) )
def test_multierror_fast_nursery(reg_addr, start_method, num_subactors, delay): @pytest.mark.parametrize(
"""Verify we raise a ``BaseExceptionGroup`` out of a nursery where '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,
):
'''
Verify we raise a ``BaseExceptionGroup`` out of a nursery where
more then one actor errors and also with a delay before failure more then one actor errors and also with a delay before failure
to test failure during an ongoing spawning. to test failure during an ongoing spawning.
"""
'''
async def main(): async def main():
async with tractor.open_nursery( async with tractor.open_nursery(
registry_addrs=[reg_addr], registry_addrs=[reg_addr],
@ -189,8 +221,15 @@ async def do_nothing():
pass pass
@pytest.mark.parametrize('mechanism', ['nursery_cancel', KeyboardInterrupt]) @pytest.mark.parametrize(
def test_cancel_single_subactor(reg_addr, mechanism): 'mechanism', [
'nursery_cancel',
KeyboardInterrupt,
])
def test_cancel_single_subactor(
reg_addr: tuple,
mechanism: str|KeyboardInterrupt,
):
''' '''
Ensure a ``ActorNursery.start_actor()`` spawned subactor Ensure a ``ActorNursery.start_actor()`` spawned subactor
cancels when the nursery is cancelled. cancels when the nursery is cancelled.
@ -232,9 +271,12 @@ async def stream_forever():
await trio.sleep(0.01) await trio.sleep(0.01)
@tractor_test @tractor_test(
async def test_cancel_infinite_streamer(start_method): timeout=6,
)
async def test_cancel_infinite_streamer(
start_method: str
):
# stream for at most 1 seconds # stream for at most 1 seconds
with ( with (
trio.fail_after(4), trio.fail_after(4),
@ -257,6 +299,14 @@ async def test_cancel_infinite_streamer(start_method):
assert n.cancelled assert n.cancelled
# @pytest.mark.skipon_spawn_backend(
# 'subint',
# reason=(
# 'XXX SUBINT HANGING TEST XXX\n'
# 'See oustanding issue(s)\n'
# # TODO, put issue link!
# )
# )
@pytest.mark.parametrize( @pytest.mark.parametrize(
'num_actors_and_errs', 'num_actors_and_errs',
[ [
@ -286,7 +336,9 @@ async def test_cancel_infinite_streamer(start_method):
'no_daemon_actors_fail_all_run_in_actors_sleep_then_fail', 'no_daemon_actors_fail_all_run_in_actors_sleep_then_fail',
], ],
) )
@tractor_test @tractor_test(
timeout=10,
)
async def test_some_cancels_all( async def test_some_cancels_all(
num_actors_and_errs: tuple, num_actors_and_errs: tuple,
start_method: str, start_method: str,
@ -370,7 +422,10 @@ async def test_some_cancels_all(
pytest.fail("Should have gotten a remote assertion error?") pytest.fail("Should have gotten a remote assertion error?")
async def spawn_and_error(breadth, depth) -> None: async def spawn_and_error(
breadth: int,
depth: int,
) -> None:
name = tractor.current_actor().name name = tractor.current_actor().name
async with tractor.open_nursery() as nursery: async with tractor.open_nursery() as nursery:
for i in range(breadth): for i in range(breadth):
@ -396,7 +451,10 @@ async def spawn_and_error(breadth, depth) -> None:
@tractor_test @tractor_test
async def test_nested_multierrors(loglevel, start_method): async def test_nested_multierrors(
loglevel: str,
start_method: str,
):
''' '''
Test that failed actor sets are wrapped in `BaseExceptionGroup`s. This Test that failed actor sets are wrapped in `BaseExceptionGroup`s. This
test goes only 2 nurseries deep but we should eventually have tests test goes only 2 nurseries deep but we should eventually have tests
@ -483,20 +541,21 @@ async def test_nested_multierrors(loglevel, start_method):
@no_windows @no_windows
def test_cancel_via_SIGINT( def test_cancel_via_SIGINT(
loglevel, loglevel: str,
start_method, start_method: str,
spawn_backend,
): ):
"""Ensure that a control-C (SIGINT) signal cancels both the parent and '''
Ensure that a control-C (SIGINT) signal cancels both the parent and
child processes in trionic fashion child processes in trionic fashion
"""
'''
pid: int = os.getpid() pid: int = os.getpid()
async def main(): async def main():
with trio.fail_after(2): with trio.fail_after(2):
async with tractor.open_nursery() as tn: async with tractor.open_nursery() as tn:
await tn.start_actor('sucka') await tn.start_actor('sucka')
if 'mp' in spawn_backend: if 'mp' in start_method:
time.sleep(0.1) time.sleep(0.1)
os.kill(pid, signal.SIGINT) os.kill(pid, signal.SIGINT)
await trio.sleep_forever() await trio.sleep_forever()
@ -580,6 +639,14 @@ async def spawn_sub_with_sync_blocking_task():
print('exiting first subactor layer..\n') print('exiting first subactor layer..\n')
# @pytest.mark.skipon_spawn_backend(
# 'subint',
# reason=(
# 'XXX SUBINT HANGING TEST XXX\n'
# 'See oustanding issue(s)\n'
# # TODO, put issue link!
# )
# )
@pytest.mark.parametrize( @pytest.mark.parametrize(
'man_cancel_outer', 'man_cancel_outer',
[ [
@ -694,7 +761,7 @@ def test_cancel_while_childs_child_in_sync_sleep(
def test_fast_graceful_cancel_when_spawn_task_in_soft_proc_wait_for_daemon( def test_fast_graceful_cancel_when_spawn_task_in_soft_proc_wait_for_daemon(
start_method, start_method: str,
): ):
''' '''
This is a very subtle test which demonstrates how cancellation This is a very subtle test which demonstrates how cancellation

View File

@ -26,6 +26,15 @@ from tractor._testing import (
from .conftest import cpu_scaling_factor from .conftest import cpu_scaling_factor
pytestmark = pytest.mark.skipon_spawn_backend(
'subint',
reason=(
'XXX SUBINT GIL-CONTENTION HANGING TEST XXX\n'
'See oustanding issue(s)\n'
# TODO, put issue link!
)
)
# XXX TODO cases: # XXX TODO cases:
# - [x] WE cancelled the peer and thus should not see any raised # - [x] WE cancelled the peer and thus should not see any raised
# `ContextCancelled` as it should be reaped silently? # `ContextCancelled` as it should be reaped silently?

View File

@ -7,6 +7,14 @@ import tractor
from tractor.experimental import msgpub from tractor.experimental import msgpub
from tractor._testing import tractor_test from tractor._testing import tractor_test
pytestmark = pytest.mark.skipon_spawn_backend(
'subint',
reason=(
'XXX SUBINT HANGING TEST XXX\n'
'See oustanding issue(s)\n'
# TODO, put issue link!
)
)
def test_type_checks(): def test_type_checks():

View File

@ -14,6 +14,14 @@ from tractor.ipc._shm import (
attach_shm_list, attach_shm_list,
) )
pytestmark = pytest.mark.skipon_spawn_backend(
'subint',
reason=(
'XXX SUBINT GIL-CONTENTION HANGING TEST XXX\n'
'See oustanding issue(s)\n'
# TODO, put issue link!
)
)
@tractor.context @tractor.context
async def child_attach_shml_alot( async def child_attach_shml_alot(

View File

@ -161,6 +161,14 @@ def test_subint_happy_teardown(
trio.run(partial(_happy_path, reg_addr, deadline)) trio.run(partial(_happy_path, reg_addr, deadline))
@pytest.mark.skipon_spawn_backend(
'subint',
reason=(
'XXX SUBINT HANGING TEST XXX\n'
'See oustanding issue(s)\n'
# TODO, put issue link!
)
)
# Wall-clock bound via `pytest-timeout` (`method='thread'`) # Wall-clock bound via `pytest-timeout` (`method='thread'`)
# as defense-in-depth over the inner `trio.fail_after(15)`. # as defense-in-depth over the inner `trio.fail_after(15)`.
# Under the orphaned-channel hang class described in # Under the orphaned-channel hang class described in