Add `subint_forkserver_proc` stub, flip dispatch, prune

Reduce `_subint_forkserver.py` to its variant-2 placeholder shape:

- Add `subint_forkserver_proc` async stub raising `NotImplementedError`
  with a redirect msg pointing at the working variant-1 backend
  (`main_thread_forkserver`), jcrist/msgspec#1026 (upstream PEP 684
  blocker), and #379 (subint umbrella).

- `tractor.spawn._spawn._methods['subint_forkserver']` now dispatches to
  the stub instead of aliasing the variant-1 coroutine
  — `--spawn-backend=subint_forkserver` errors cleanly.

- Drop now-dead module-scope: `ChildSigintMode`
  / `_DEFAULT_CHILD_SIGINT` defs, `_has_subints` try/except (replaced
  with import from `._subint`), unused imports (`partial`, `Literal`,
  `sys`, msgtypes/pretty_struct, `current_actor`,
  `cancel_on_completion`/`soft_kill`, `_server` TYPE_CHECKING).

- Backward-compat re-exports of fork primitives kept until the follow-up
  commit migrates external test imports.

- `tests/spawn/test_subint_forkserver.py::forkserver_spawn_method`
  fixture: flip hardcoded `'subint_forkserver'`
  → `'main_thread_forkserver'` so the test still exercises the working
  backend (full file rename comes in the test-import migration commit).

(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:36:08 -04:00
parent 57dae0e4a6
commit 5e83881f10
3 changed files with 82 additions and 67 deletions

View File

@ -283,8 +283,8 @@ async def _happy_path_forkserver(
def forkserver_spawn_method(): def forkserver_spawn_method():
''' '''
Flip `tractor.spawn._spawn._spawn_method` to Flip `tractor.spawn._spawn._spawn_method` to
`'subint_forkserver'` for the duration of a test, then `'main_thread_forkserver'` for the duration of a test,
restore whatever was in place before (usually the then restore whatever was in place before (usually the
session-level CLI choice, typically `'trio'`). session-level CLI choice, typically `'trio'`).
Without this, other tests in the same session would Without this, other tests in the same session would
@ -295,7 +295,7 @@ def forkserver_spawn_method():
''' '''
prev_method: str = _spawn_mod._spawn_method prev_method: str = _spawn_mod._spawn_method
prev_ctx = _spawn_mod._ctx prev_ctx = _spawn_mod._ctx
try_set_start_method('subint_forkserver') try_set_start_method('main_thread_forkserver')
try: try:
yield yield
finally: finally:

View File

@ -494,6 +494,7 @@ from ._mp import mp_proc
from ._subint import subint_proc from ._subint import subint_proc
from ._subint_fork import subint_fork_proc from ._subint_fork import subint_fork_proc
from ._main_thread_forkserver import main_thread_forkserver_proc from ._main_thread_forkserver import main_thread_forkserver_proc
from ._subint_forkserver import subint_forkserver_proc
# proc spawning backend target map # proc spawning backend target map
@ -517,10 +518,9 @@ _methods: dict[SpawnMethodKey, Callable] = {
# Variant-2 (future, reserved): same fork machinery but # Variant-2 (future, reserved): same fork machinery but
# child enters a sub-interpreter to host its `trio.run()` # child enters a sub-interpreter to host its `trio.run()`
# — gated on jcrist/msgspec#1026 unblocking PEP 684 # — gated on jcrist/msgspec#1026 unblocking PEP 684
# isolated-mode subints. Today aliases to the variant-1 # isolated-mode subints. Today the stub raises
# impl so `--spawn-backend=subint_forkserver` keeps # `NotImplementedError` pointing at the variant-1 backend
# working; flipped to a `NotImplementedError` stub in a # + upstream blocker. See
# follow-up commit. See
# `tractor.spawn._subint_forkserver`. # `tractor.spawn._subint_forkserver`.
'subint_forkserver': main_thread_forkserver_proc, 'subint_forkserver': subint_forkserver_proc,
} }

View File

@ -142,12 +142,9 @@ See also
''' '''
from __future__ import annotations from __future__ import annotations
import sys
import threading import threading
from functools import partial
from typing import ( from typing import (
Any, Any,
Literal,
TYPE_CHECKING, TYPE_CHECKING,
) )
@ -155,78 +152,42 @@ import trio
from trio import TaskStatus from trio import TaskStatus
from tractor.log import get_logger from tractor.log import get_logger
from tractor.msg import ( from ._subint import _has_subints
types as msgtypes,
pretty_struct, # Backward-compat re-exports of the fork primitives whose
) # canonical home is now `_main_thread_forkserver`. Kept here
from tractor.runtime._state import current_actor # transiently so existing
from tractor.runtime._portal import Portal # `from tractor.spawn._subint_forkserver import ...` callsites
from ._spawn import ( # in the tests + the conc-anal smoketest keep resolving;
cancel_on_completion, # dropped once a follow-up commit migrates those imports to
soft_kill, # the new module.
)
# Lower-level fork primitives — see module docstring for the
# split rationale. `_subint_forkserver` builds tractor's
# subint-family spawn backend on top of these.
from ._main_thread_forkserver import ( from ._main_thread_forkserver import (
_close_inherited_fds as _close_inherited_fds, _close_inherited_fds as _close_inherited_fds,
_format_child_exit as _format_child_exit, _format_child_exit as _format_child_exit,
fork_from_worker_thread as fork_from_worker_thread, fork_from_worker_thread as fork_from_worker_thread,
wait_child as wait_child, wait_child as wait_child,
_ForkedProc, _ForkedProc as _ForkedProc,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from tractor.discovery._addr import UnwrappedAddress from tractor.discovery._addr import UnwrappedAddress
from tractor.ipc import ( from tractor.runtime._portal import Portal
_server,
)
from tractor.runtime._runtime import Actor from tractor.runtime._runtime import Actor
from tractor.runtime._supervise import ActorNursery from tractor.runtime._supervise import ActorNursery
# Private CPython subint API — used by `run_subint_in_worker_thread`
# below. Imported only when 3.14+ is detected (via `_has_subints`
# from `_subint`); on older runtimes the symbol is `None` and
# the function raises a clean `RuntimeError` on entry.
if _has_subints:
import _interpreters # type: ignore
else:
_interpreters = None # type: ignore
log = get_logger('tractor') log = get_logger('tractor')
# Configurable child-side SIGINT handling for forkserver-spawned
# subactors. Threaded through `subint_forkserver_proc`'s
# `proc_kwargs` under the `'child_sigint'` key.
#
# - `'ipc'` (default, currently the only implemented mode):
# child has NO trio-level SIGINT handler — trio.run() is on
# the fork-inherited non-main thread, `signal.set_wakeup_fd()`
# is main-thread-only. Cancellation flows exclusively via
# the parent's `Portal.cancel_actor()` IPC path. Safe +
# deterministic for nursery-structured apps where the parent
# is always the cancel authority. Known gap: orphan
# (post-parent-SIGKILL) children don't respond to SIGINT
# — see `test_orphaned_subactor_sigint_cleanup_DRAFT`.
#
# - `'trio'` (**not yet implemented**): install a manual
# SIGINT → trio-cancel bridge in the child's fork prelude
# (pre-`trio.run()`) so external Ctrl-C reaches stuck
# grandchildren even with a dead parent. Adds signal-
# handling surface the `'ipc'` default cleanly avoids; only
# pay for it when externally-interruptible children actually
# matter (e.g. CLI tool grandchildren).
ChildSigintMode = Literal['ipc', 'trio']
_DEFAULT_CHILD_SIGINT: ChildSigintMode = 'ipc'
# Feature-gate: py3.14+ via the public `concurrent.interpreters`
# wrapper. Matches the gate in `tractor.spawn._subint` —
# see that module's docstring for why we require the public
# API's presence even though we reach into the private
# `_interpreters` C module for actual calls.
try:
from concurrent import interpreters as _public_interpreters # noqa: F401 # type: ignore
import _interpreters # type: ignore
_has_subints: bool = True
except ImportError:
_interpreters = None # type: ignore
_has_subints: bool = False
def run_subint_in_worker_thread( def run_subint_in_worker_thread(
bootstrap: str, bootstrap: str,
*, *,
@ -308,3 +269,57 @@ def run_subint_in_worker_thread(
) )
if err is not None: if err is not None:
raise err raise err
async def subint_forkserver_proc(
name: str,
actor_nursery: ActorNursery,
subactor: Actor,
errors: dict[tuple[str, str], Exception],
bind_addrs: list[UnwrappedAddress],
parent_addr: UnwrappedAddress,
_runtime_vars: dict[str, Any],
*,
infect_asyncio: bool = False,
task_status: TaskStatus[Portal] = trio.TASK_STATUS_IGNORED,
proc_kwargs: dict[str, any] = {},
) -> None:
'''
PLACEHOLDER variant-2 (subint-isolated child runtime)
spawn-backend coroutine. Reserved for the eventual impl
that uses `run_subint_in_worker_thread()` in the fork-child
to host the child's `trio.run()` inside a fresh subint.
Today this stub raises immediately so
`--spawn-backend=subint_forkserver` errors cleanly with a
pointer to the working variant-1 backend
(`main_thread_forkserver`) and the upstream blocker
([jcrist/msgspec#1026](https://github.com/jcrist/msgspec/issues/1026)).
See this module's top-level docstring for the future-arch
design + what lives here when the variant-2 impl lands.
'''
raise NotImplementedError(
f'`{ "subint_forkserver"!r}` spawn backend is reserved '
f'for the future variant-2 (subint-isolated child '
f'runtime) — gated on jcrist/msgspec#1026 unblocking '
f'PEP 684 isolated-mode subints upstream.\n'
f'\n'
f'For the working fork-based backend today, use '
f'`--spawn-backend=main_thread_forkserver` (variant '
f'1: fork from a regular main-interp worker thread, '
f'child runs trio on its own main interp).\n'
f'\n'
f'See:\n'
f' - tractor.spawn._main_thread_forkserver — the '
f'working variant-1 impl + design rationale\n'
f' - tractor.spawn._subint_forkserver — this '
f'module\'s docstring for the variant-2 future-arch\n'
f' - https://github.com/goodboy/tractor/issues/379 '
f'(subint umbrella)\n'
f' - https://github.com/jcrist/msgspec/issues/1026 '
f'(upstream PEP 684 blocker)'
)