diff --git a/pyproject.toml b/pyproject.toml index 5d46a6c8..6f14751a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -217,7 +217,7 @@ addopts = [ '--show-capture=no', # 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 # children inherit those fds — opaque deadlocks happen in # the pytest-capture-machinery ↔ fork-child stdio diff --git a/tests/discovery/test_registrar.py b/tests/discovery/test_registrar.py index 618c93ad..d7fa15c2 100644 --- a/tests/discovery/test_registrar.py +++ b/tests/discovery/test_registrar.py @@ -538,13 +538,13 @@ async def kill_transport( @pytest.mark.timeout( 30, # NOTE should be a 2.1s happy path. - # XXX for `subint_forkserver` this is SUPER SENSITIVE so keep it - # higher to avoid flaky runs.. + # XXX for `main_thread_forkserver` this is SUPER SENSITIVE + # so keep it higher to avoid flaky runs.. method='thread', ) @pytest.mark.skipon_spawn_backend( 'subint', - # 'subint_forkserver', + # 'main_thread_forkserver', reason=( 'XXX SUBINT HANGING TEST XXX\n' 'See oustanding issue(s)\n' diff --git a/tests/test_cancellation.py b/tests/test_cancellation.py index 27a5eee2..28168fd6 100644 --- a/tests/test_cancellation.py +++ b/tests/test_cancellation.py @@ -452,8 +452,12 @@ async def spawn_and_error( await nursery.run_in_actor(*args, **kwargs) -# NOTE: subint_forkserver skip handled by file-level `pytestmark` -# above (same pytest-capture-fd hang class as siblings). +# 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. @pytest.mark.timeout( 10, method='thread', diff --git a/tests/test_infected_asyncio.py b/tests/test_infected_asyncio.py index e13df325..d3524a6b 100644 --- a/tests/test_infected_asyncio.py +++ b/tests/test_infected_asyncio.py @@ -1113,7 +1113,7 @@ def test_sigint_closes_lifetime_stack( if ( send_sigint_to == 'child' and - start_method == 'subint_forkserver' + start_method == 'main_thread_forkserver' ): pytest.xfail( reason=( diff --git a/tests/test_shm.py b/tests/test_shm.py index d6ad93f4..84d0988e 100644 --- a/tests/test_shm.py +++ b/tests/test_shm.py @@ -16,22 +16,16 @@ from tractor.ipc._shm import ( pytestmark = pytest.mark.skipon_spawn_backend( 'subint', - # 'subint_forkserver', - # XXX we hack around this stdlib limitation by both, - # - setting `ShareMemory(track=False)` - # - overriding the `mp.ResourceTracker` nonsense in - # `.ipc._mp_bs`. + # NOTE, `main_thread_forkserver` works for these tests + # via the `mp.SharedMemory(track=False)` + + # `mp.resource_tracker` monkey-patch in `.ipc._mp_bs`. + # Without that workaround the fork-inherited + # `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=( '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' ) ) diff --git a/tests/test_spawning.py b/tests/test_spawning.py index b0e8a88d..63a2fb8e 100644 --- a/tests/test_spawning.py +++ b/tests/test_spawning.py @@ -194,7 +194,7 @@ def test_loglevel_propagated_to_subactor( reg_addr: tuple, level: str, ): - if start_method in ('mp_forkserver', 'subint_forkserver'): + if start_method in ('mp_forkserver', 'main_thread_forkserver'): pytest.skip( "a bug with `capfd` seems to make forkserver capture not work? " "(same class as the `mp_forkserver` pre-existing skip — fork-" diff --git a/tractor/_root.py b/tractor/_root.py index 3c20fff0..233a89d7 100644 --- a/tractor/_root.py +++ b/tractor/_root.py @@ -79,7 +79,7 @@ _DEBUG_COMPATIBLE_BACKENDS: tuple[str, ...] = ( 'trio', # forkserver children run `_trio_main` in their own OS # process — same child-side runtime shape as `trio_proc`. - 'subint_forkserver', + 'main_thread_forkserver', ) diff --git a/tractor/_testing/pytest.py b/tractor/_testing/pytest.py index c707e0db..801ecafc 100644 --- a/tractor/_testing/pytest.py +++ b/tractor/_testing/pytest.py @@ -303,7 +303,7 @@ def _reap_orphaned_subactors(): grace window, then SIGKILL survivors. 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 this reap, they linger across sessions and compete for ports / inherit pytest's capture-pipe fds — which diff --git a/tractor/runtime/_runtime.py b/tractor/runtime/_runtime.py index 38889229..8ab55df0 100644 --- a/tractor/runtime/_runtime.py +++ b/tractor/runtime/_runtime.py @@ -1763,7 +1763,7 @@ async def async_main( # shielded loop would park on the parent chan # indefinitely waiting for EOF that only arrives # 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. actor._parent_chan_cs = await root_tn.start( partial( diff --git a/tractor/runtime/_state.py b/tractor/runtime/_state.py index aedcc952..b9316448 100644 --- a/tractor/runtime/_state.py +++ b/tractor/runtime/_state.py @@ -122,8 +122,8 @@ class RuntimeVars(Struct): # `open_root_actor()` nor received a parent `SpawnSpec`. Kept # as a module-level constant so `get_runtime_vars(clear_values= # True)` can reset the live dict back to this baseline (see -# `tractor.spawn._subint_forkserver` for the one current caller -# that needs it). +# `tractor.spawn._main_thread_forkserver` for the one current +# caller that needs it). _RUNTIME_VARS_DEFAULTS: dict[str, Any] = { # root of actor-process tree info '_is_root': False, # bool @@ -165,7 +165,7 @@ def get_runtime_vars( defaults (`_RUNTIME_VARS_DEFAULTS`) instead of the live dict. Useful in combination with `set_runtime_vars()` to 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: set_runtime_vars(get_runtime_vars(clear_values=True)) diff --git a/tractor/spawn/_subint.py b/tractor/spawn/_subint.py index d3e4e38f..5ee6aaa8 100644 --- a/tractor/spawn/_subint.py +++ b/tractor/spawn/_subint.py @@ -257,7 +257,8 @@ async def subint_proc( # via a `nonlocal err` slot inspected after # `subint_exited.wait()` — see anyio's # `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 # now: re-raise from the parent must coordinate with # the existing `trio.Cancelled` paths around the