Sweep `subint_forkserver` → `main_thread_forkserver` in code

After the variant-1 / variant-2 backend split, update remaining
string-match refs to the variant-1 backend so user-visible gates
+ skip-marks + comments name the working backend correctly:

- `tractor._root._DEBUG_COMPATIBLE_BACKENDS`: include
  `main_thread_forkserver`, drop the stub-only `subint_forkserver`
  entry.
- `tests/test_spawning.py::test_loglevel_propagated_to_subactor`:
  capfd-skip flips to `main_thread_forkserver`.
- `tests/test_infected_asyncio.py::test_sigint_closes_lifetime_stack`:
  xfail-condition flips to `main_thread_forkserver`.
- `tests/test_shm.py`: drop stale "broken on `main_thread_forkserver`"
  reason-text since the `mp.SharedMemory(track=False)`
  + resource-tracker monkey-patch in `.ipc._mp_bs` makes the tests pass;
  the skip-mark only fires on plain `subint` now.
- Comment / docstring sweep: `runtime._state`, `runtime._runtime`,
  `_testing.pytest`, `_subint.py`, `pyproject.toml`,
  `test_cancellation.py`, `test_registrar.py` — refs to variant-1
  backend updated.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
subint_forkserver_backend
Gud Boi 2026-04-27 19:55:37 -04:00
parent 9f0709eee2
commit 205382a39b
11 changed files with 28 additions and 29 deletions

View File

@ -217,7 +217,7 @@ addopts = [
'--show-capture=no', '--show-capture=no',
# sys-level capture. REQUIRED for fork-based spawn # sys-level capture. REQUIRED for fork-based spawn
# backends (e.g. `subint_forkserver`): default # backends (e.g. `main_thread_forkserver`): default
# `--capture=fd` redirects fd 1,2 to temp files, and fork # `--capture=fd` redirects fd 1,2 to temp files, and fork
# children inherit those fds — opaque deadlocks happen in # children inherit those fds — opaque deadlocks happen in
# the pytest-capture-machinery ↔ fork-child stdio # the pytest-capture-machinery ↔ fork-child stdio

View File

@ -538,13 +538,13 @@ async def kill_transport(
@pytest.mark.timeout( @pytest.mark.timeout(
30, 30,
# NOTE should be a 2.1s happy path. # NOTE should be a 2.1s happy path.
# XXX for `subint_forkserver` this is SUPER SENSITIVE so keep it # XXX for `main_thread_forkserver` this is SUPER SENSITIVE
# higher to avoid flaky runs.. # so keep it higher to avoid flaky runs..
method='thread', method='thread',
) )
@pytest.mark.skipon_spawn_backend( @pytest.mark.skipon_spawn_backend(
'subint', 'subint',
# 'subint_forkserver', # 'main_thread_forkserver',
reason=( reason=(
'XXX SUBINT HANGING TEST XXX\n' 'XXX SUBINT HANGING TEST XXX\n'
'See oustanding issue(s)\n' 'See oustanding issue(s)\n'

View File

@ -452,8 +452,12 @@ async def spawn_and_error(
await nursery.run_in_actor(*args, **kwargs) await nursery.run_in_actor(*args, **kwargs)
# NOTE: subint_forkserver skip handled by file-level `pytestmark` # NOTE: `main_thread_forkserver` capture-fd hang class is no
# above (same pytest-capture-fd hang class as siblings). # 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.
@pytest.mark.timeout( @pytest.mark.timeout(
10, 10,
method='thread', method='thread',

View File

@ -1113,7 +1113,7 @@ def test_sigint_closes_lifetime_stack(
if ( if (
send_sigint_to == 'child' send_sigint_to == 'child'
and and
start_method == 'subint_forkserver' start_method == 'main_thread_forkserver'
): ):
pytest.xfail( pytest.xfail(
reason=( reason=(

View File

@ -16,22 +16,16 @@ from tractor.ipc._shm import (
pytestmark = pytest.mark.skipon_spawn_backend( pytestmark = pytest.mark.skipon_spawn_backend(
'subint', 'subint',
# 'subint_forkserver', # NOTE, `main_thread_forkserver` works for these tests
# XXX we hack around this stdlib limitation by both, # via the `mp.SharedMemory(track=False)` +
# - setting `ShareMemory(track=False)` # `mp.resource_tracker` monkey-patch in `.ipc._mp_bs`.
# - overriding the `mp.ResourceTracker` nonsense in # Without that workaround the fork-inherited
# `.ipc._mp_bs`. # `resource_tracker` fd would EBADF on first shm op +
# cascade into `FileExistsError` across parametrize
# variants. Tracker doc:
# `ai/conc-anal/subint_forkserver_mp_shared_memory_issue.md`.
reason=( reason=(
'subint: GIL-contention hanging class.\n' 'subint: GIL-contention hanging class.\n'
'subint_forkserver: `multiprocessing.SharedMemory` '
'is fork-without-exec unsafe — child inherits parent\'s '
'`resource_tracker` fd → EBADF on first shm op '
'(`test_child_attaches_alot`); leaked `/shm_list` from '
'a "passing" run cascades into `FileExistsError` across '
'parametrize variants (`test_parent_writer_child_reader`). '
'Canonical CPython issue class, NOT a tractor bug; full '
'tracker doc:\n'
'ai/conc-anal/subint_forkserver_mp_shared_memory_issue.md'
) )
) )

View File

@ -194,7 +194,7 @@ def test_loglevel_propagated_to_subactor(
reg_addr: tuple, reg_addr: tuple,
level: str, level: str,
): ):
if start_method in ('mp_forkserver', 'subint_forkserver'): if start_method in ('mp_forkserver', 'main_thread_forkserver'):
pytest.skip( pytest.skip(
"a bug with `capfd` seems to make forkserver capture not work? " "a bug with `capfd` seems to make forkserver capture not work? "
"(same class as the `mp_forkserver` pre-existing skip — fork-" "(same class as the `mp_forkserver` pre-existing skip — fork-"

View File

@ -79,7 +79,7 @@ _DEBUG_COMPATIBLE_BACKENDS: tuple[str, ...] = (
'trio', 'trio',
# forkserver children run `_trio_main` in their own OS # forkserver children run `_trio_main` in their own OS
# process — same child-side runtime shape as `trio_proc`. # process — same child-side runtime shape as `trio_proc`.
'subint_forkserver', 'main_thread_forkserver',
) )

View File

@ -303,7 +303,7 @@ def _reap_orphaned_subactors():
grace window, then SIGKILL survivors. grace window, then SIGKILL survivors.
Rationale: under fork-based spawn backends (notably Rationale: under fork-based spawn backends (notably
`subint_forkserver`), a test that times out or bails `main_thread_forkserver`), a test that times out or bails
mid-teardown can leave subactor forks alive. Without mid-teardown can leave subactor forks alive. Without
this reap, they linger across sessions and compete this reap, they linger across sessions and compete
for ports / inherit pytest's capture-pipe fds — which for ports / inherit pytest's capture-pipe fds — which

View File

@ -1763,7 +1763,7 @@ async def async_main(
# shielded loop would park on the parent chan # shielded loop would park on the parent chan
# indefinitely waiting for EOF that only arrives # indefinitely waiting for EOF that only arrives
# after the PARENT tears down, which under # after the PARENT tears down, which under
# fork-based backends (e.g. `subint_forkserver`) # fork-based backends (e.g. `main_thread_forkserver`)
# it waits on THIS actor's exit — deadlock. # it waits on THIS actor's exit — deadlock.
actor._parent_chan_cs = await root_tn.start( actor._parent_chan_cs = await root_tn.start(
partial( partial(

View File

@ -122,8 +122,8 @@ class RuntimeVars(Struct):
# `open_root_actor()` nor received a parent `SpawnSpec`. Kept # `open_root_actor()` nor received a parent `SpawnSpec`. Kept
# as a module-level constant so `get_runtime_vars(clear_values= # as a module-level constant so `get_runtime_vars(clear_values=
# True)` can reset the live dict back to this baseline (see # True)` can reset the live dict back to this baseline (see
# `tractor.spawn._subint_forkserver` for the one current caller # `tractor.spawn._main_thread_forkserver` for the one current
# that needs it). # caller that needs it).
_RUNTIME_VARS_DEFAULTS: dict[str, Any] = { _RUNTIME_VARS_DEFAULTS: dict[str, Any] = {
# root of actor-process tree info # root of actor-process tree info
'_is_root': False, # bool '_is_root': False, # bool
@ -165,7 +165,7 @@ def get_runtime_vars(
defaults (`_RUNTIME_VARS_DEFAULTS`) instead of the live defaults (`_RUNTIME_VARS_DEFAULTS`) instead of the live
dict. Useful in combination with `set_runtime_vars()` to dict. Useful in combination with `set_runtime_vars()` to
reset process-global state back to "cold" the main caller reset process-global state back to "cold" the main caller
today is the `subint_forkserver` spawn backend's post-fork today is the `main_thread_forkserver` spawn backend's post-fork
child prelude: child prelude:
set_runtime_vars(get_runtime_vars(clear_values=True)) set_runtime_vars(get_runtime_vars(clear_values=True))

View File

@ -257,7 +257,8 @@ async def subint_proc(
# via a `nonlocal err` slot inspected after # via a `nonlocal err` slot inspected after
# `subint_exited.wait()` — see anyio's # `subint_exited.wait()` — see anyio's
# `to_interpreter._interp_call` `(retval, is_exception)` # `to_interpreter._interp_call` `(retval, is_exception)`
# tuple pattern + `_subint_forkserver.py:480-494`'s # tuple pattern +
# `_subint_forkserver.run_subint_in_worker_thread._drive`'s
# equivalent which already does this. Skipped here for # equivalent which already does this. Skipped here for
# now: re-raise from the parent must coordinate with # now: re-raise from the parent must coordinate with
# the existing `trio.Cancelled` paths around the # the existing `trio.Cancelled` paths around the