tractor/tractor/spawn/_spawn.py

627 lines
20 KiB
Python
Raw Normal View History

Re-license code base for distribution under AGPL This commit obviously denotes a re-license of all applicable parts of the code base. Acknowledgement of this change was completed in #274 by the majority of the current set of contributors. From here henceforth all changes will be AGPL licensed and distributed. This is purely an effort to maintain the same copy-left policy whilst closing the (perceived) SaaS loophole the GPL allows for. It is merely for this loophole: to avoid code hiding by any potential "network providers" who are attempting to use the project to make a profit without either compensating the authors or re-distributing their changes. I thought quite a bit about this change and can't see a reason not to close the SaaS loophole in our current license. We still are (hard) copy-left and I plan to keep the code base this way for a couple reasons: - The code base produces income/profit through parent projects and is demonstrably of high value. - I believe firms should not get free lunch for the sake of "contributions from their employees" or "usage as a service" which I have found to be a dubious argument at best. - If a firm who intends to profit from the code base wants to use it they can propose a secondary commercial license to purchase with the proceeds going to the project's authors under some form of well defined contract. - Many successful projects like Qt use this model; I see no reason it can't work in this case until such a time as the authors feel it should be loosened. There has been detailed discussion in #103 on licensing alternatives. The main point of this AGPL change is to protect the code base for the time being from exploitation while it grows and as we move into the next phase of development which will include extension into the multi-host distributed software space.
2021-12-13 18:08:32 +00:00
# tractor: structured concurrent "actors".
# Copyright 2018-eternity Tyler Goodlet.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Top level routines & machinery for actor-as-process/subint spawning
over multiple backends.
"""
from __future__ import annotations
import multiprocessing as mp
2020-01-20 16:10:51 +00:00
import platform
Add `'subint'` spawn backend scaffold (#379) Land the scaffolding for a future sub-interpreter (PEP 734 `concurrent.interpreters`) actor spawn backend per issue #379. The spawn flow itself is not yet implemented; `subint_proc()` raises a placeholder `NotImplementedError` pointing at the tracking issue — this commit only wires up the registry, the py-version gate, and the harness. Deats, - bump `pyproject.toml` `requires-python` to `>=3.12, <3.15` and list the `3.14` classifier — the new stdlib `concurrent.interpreters` module only ships on 3.14 - extend `SpawnMethodKey = Literal[..., 'subint']` - `try_set_start_method('subint')` grows a new `match` arm that feature-detects the stdlib module and raises `RuntimeError` with a clear banner on py<3.14 - `_methods` registers the new `subint_proc()` via the same bottom-of-module late-import pattern used for `._trio` / `._mp` Also, - new `tractor/spawn/_subint.py` — top-level `try: from concurrent import interpreters` guards `_has_subints: bool`; `subint_proc()` signature mirrors `trio_proc`/`mp_proc` so the Phase B.2 impl can drop in without touching the registry - re-add `import sys` to `_spawn.py` (needed for the py-version msg in the gate-error) - `_testing.pytest.pytest_configure` wraps `try_set_start_method()` in a `pytest.UsageError` handler so `--spawn-backend=subint` on py<3.14 prints a clean banner instead of a traceback (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit d318f1f8f4f6a056a8ca67d7f15e4e2a72507ea3) (MTF-only portion: kept tractor/spawn/_spawn.py tractor/spawn/_subint.py)
2026-06-16 00:04:25 +00:00
import sys
2021-12-02 17:34:27 +00:00
from typing import (
Any,
Awaitable,
Literal,
Callable,
TypeVar,
TYPE_CHECKING,
2021-12-02 17:34:27 +00:00
)
import trio
from trio import TaskStatus
2019-11-26 14:23:37 +00:00
from ..devx import debug
from tractor.runtime._state import (
_runtime_vars,
)
from tractor.log import get_logger
from tractor.discovery._addr import (
UnwrappedAddress,
)
from .._exceptions import ActorFailure
from ._reap import unlink_uds_bind_addrs
from tractor.runtime._portal import Portal
from tractor.runtime._runtime import Actor
from tractor.msg import types as msgtypes
2020-01-20 16:10:51 +00:00
if TYPE_CHECKING:
from tractor.ipc import (
Channel,
)
from tractor.runtime._supervise import ActorNursery
ProcessType = TypeVar('ProcessType', mp.Process, trio.Process)
2020-01-20 16:10:51 +00:00
log = get_logger('tractor')
# placeholder for an mp start context if so using that backend
_ctx: mp.context.BaseContext | None = None
SpawnMethodKey = Literal[
'trio', # supported on all platforms
'mp_spawn',
'mp_forkserver', # posix only
Add `'subint'` spawn backend scaffold (#379) Land the scaffolding for a future sub-interpreter (PEP 734 `concurrent.interpreters`) actor spawn backend per issue #379. The spawn flow itself is not yet implemented; `subint_proc()` raises a placeholder `NotImplementedError` pointing at the tracking issue — this commit only wires up the registry, the py-version gate, and the harness. Deats, - bump `pyproject.toml` `requires-python` to `>=3.12, <3.15` and list the `3.14` classifier — the new stdlib `concurrent.interpreters` module only ships on 3.14 - extend `SpawnMethodKey = Literal[..., 'subint']` - `try_set_start_method('subint')` grows a new `match` arm that feature-detects the stdlib module and raises `RuntimeError` with a clear banner on py<3.14 - `_methods` registers the new `subint_proc()` via the same bottom-of-module late-import pattern used for `._trio` / `._mp` Also, - new `tractor/spawn/_subint.py` — top-level `try: from concurrent import interpreters` guards `_has_subints: bool`; `subint_proc()` signature mirrors `trio_proc`/`mp_proc` so the Phase B.2 impl can drop in without touching the registry - re-add `import sys` to `_spawn.py` (needed for the py-version msg in the gate-error) - `_testing.pytest.pytest_configure` wraps `try_set_start_method()` in a `pytest.UsageError` handler so `--spawn-backend=subint` on py<3.14 prints a clean banner instead of a traceback (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit d318f1f8f4f6a056a8ca67d7f15e4e2a72507ea3) (MTF-only portion: kept tractor/spawn/_spawn.py tractor/spawn/_subint.py)
2026-06-16 00:04:25 +00:00
'subint', # py3.14+ via `concurrent.interpreters` (PEP 734)
Doc `subint_fork` as blocked by CPython post-fork Empirical finding: the WIP `subint_fork_proc` scaffold landed in `cf0e3e6f` does *not* work on current CPython. The `fork()` syscall succeeds in the parent, but the CHILD aborts immediately during `PyOS_AfterFork_Child()` → `_PyInterpreterState_DeleteExceptMain()`, which gates on the current tstate belonging to the main interp — the child dies with `Fatal Python error: not main interpreter`. CPython devs acknowledge the fragility with an in-source comment (`// Ideally we could guarantee tstate is running main.`) but expose no user-facing hook to satisfy the precondition — so the strategy is structurally dead until upstream changes. Rather than delete the scaffold, reshape it into a documented dead-end so the next person with this idea lands on the reason rather than rediscovering the same CPython-level refusal. Deats, - Move `subint_fork_proc` out of `tractor.spawn._subint` into a new `tractor.spawn._subint_fork` dedicated module (153 LOC). Module + fn docstrings now describe the blockage directly; the fn body is trimmed to a `NotImplementedError` pointing at the analysis doc — no more dead-code `bootstrap` sketch bloating `_subint.py`. - `_spawn.py`: keep `'subint_fork'` in `SpawnMethodKey` + the `_methods` dispatch so `--spawn-backend=subint_fork` routes to a clean `NotImplementedError` rather than "invalid backend"; comment calls out the blockage. Collapse the duplicate py3.14 feature-gate in `try_set_start_method()` into a combined `case 'subint' | 'subint_fork':` arm. - New 337-line analysis: `ai/conc-anal/subint_fork_blocked_by_cpython_post_fork_issue.md`. Annotated walkthrough from the user-visible fatal error down to the specific `Modules/posixmodule.c` + `Python/pystate.c` source lines enforcing the refusal, plus an upstream-report draft. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 0f48ed2eb9541f14cf3646e760281c212e2eaa69)
2026-04-22 20:02:01 +00:00
# EXPERIMENTAL — blocked at the CPython level. The
# design goal was a `trio+fork`-safe subproc spawn via
# `os.fork()` from a trio-free launchpad sub-interpreter,
# but CPython's `PyOS_AfterFork_Child` → `_PyInterpreterState_DeleteExceptMain`
# requires fork come from the main interp. See
# `tractor.spawn._subint_fork` +
# `ai/conc-anal/subint_fork_blocked_by_cpython_post_fork_issue.md`
# + issue #379 for the full analysis.
'subint_fork',
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
# EXPERIMENTAL — the `subint_fork` workaround. `os.fork()`
# from a non-trio worker thread (never entered a subint)
# is CPython-legal and works cleanly; forked child runs
# `tractor._child._actor_child_main()` against a trio
# runtime, exactly like `trio_proc` but via fork instead
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
# of subproc-exec. See `tractor.spawn._main_thread_forkserver`.
'main_thread_forkserver',
# Variant-2: same fork machinery as `main_thread_forkserver`
# but the child enters a sub-interpreter to host its
# `trio.run()`. Gated on jcrist/msgspec#1026 unblocking
# PEP 684 isolated-mode subints upstream — until then
# `subint_forkserver_proc` is a clean `NotImplementedError`
# stub pointing at variant-1 (`main_thread_forkserver`) +
# the upstream blocker. The key is reserved here (not just
# aliased to variant-1) so once upstream lands the impl can
# flip in-place without API churn. See
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
# `tractor.spawn._subint_forkserver`.
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
'subint_forkserver',
]
_spawn_method: SpawnMethodKey = 'trio'
2020-01-20 16:10:51 +00:00
if platform.system() == 'Windows':
_ctx = mp.get_context("spawn")
2020-01-20 16:10:51 +00:00
async def proc_waiter(proc: mp.Process) -> None:
await trio.lowlevel.WaitForSingleObject(proc.sentinel)
2020-01-20 16:10:51 +00:00
else:
# *NIX systems use ``trio`` primitives as our default as well
2020-01-23 06:23:26 +00:00
2020-01-20 16:10:51 +00:00
async def proc_waiter(proc: mp.Process) -> None:
await trio.lowlevel.wait_readable(proc.sentinel)
2020-01-20 16:10:51 +00:00
async def wait_for_peer_or_proc_death(
ipc_server,
uid: tuple[str, str],
# TODO? not not types?
proc_wait: 'Callable[[], Awaitable]',
proc_repr: str = '',
) -> 'tuple[trio.Event, Channel]':
'''
Race `IPCServer.wait_for_peer(uid)` against the sub-proc's
own `.wait()` coroutine. Whichever completes first cancels
the other.
Used by every spawn-backend to detect a sub-actor that
*dies during boot* before completing the parent-handshake-
callback (e.g. crashed on import, exec'd-out, kernel-killed
pre-`_actor_child_main`). Without this race, the
handshake-wait backed by an unsignalled `trio.Event`
parks the spawning task forever and leaves the dead child
as a zombie since nobody calls `proc.wait()` to reap.
On normal handshake-complete: returns `(event, chan)`
identical to a bare `wait_for_peer`.
On proc-death-first: raises `ActorFailure` carrying the
proc's exit code, allowing the supervisor to surface a
clean error rather than hanging indefinitely.
`proc_wait` is a 0-arg async callable returning the proc's
exit-status kept generic so each backend can pass its
own (`trio.Process.wait`, `_ForkedProc.wait`,
`proc_waiter(mp.Process)`, etc.).
`proc_repr` is an optional string used in the
`ActorFailure` message for diag.
'''
result: dict = {}
async def _await_handshake():
event, chan = await ipc_server.wait_for_peer(uid)
result['handshake'] = (event, chan)
boot_n.cancel_scope.cancel()
async def _await_death():
rc = await proc_wait()
result['died'] = rc
boot_n.cancel_scope.cancel()
async with trio.open_nursery() as boot_n:
boot_n.start_soon(_await_handshake)
boot_n.start_soon(_await_death)
if 'handshake' in result:
return result['handshake']
# only reached if proc-death won the race
raise ActorFailure(
f'Sub-actor {uid!r} died during boot '
f'(rc={result.get("died")!r}) before completing '
f'parent-handshake.\n'
f' proc: {proc_repr}'
)
def try_set_start_method(
key: SpawnMethodKey
) -> mp.context.BaseContext | None:
'''
Attempt to set the method for process starting, aka the "actor
spawning backend".
If the desired method is not supported this function will error.
On Windows only the ``multiprocessing`` "spawn" method is offered
besides the default ``trio`` which uses async wrapping around
``subprocess.Popen``.
'''
import multiprocessing as mp
global _ctx
global _spawn_method
mp_methods = mp.get_all_start_methods()
if 'fork' in mp_methods:
# forking is incompatible with ``trio``s global task tree
mp_methods.remove('fork')
match key:
case 'mp_forkserver':
from . import _forkserver_override
_forkserver_override.override_stdlib()
_ctx = mp.get_context('forkserver')
case 'mp_spawn':
_ctx = mp.get_context('spawn')
case (
'trio'
| 'main_thread_forkserver'
):
_ctx = None
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
case (
'subint'
| 'subint_fork'
| 'subint_forkserver'
):
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
# All subint-family backends need no `mp.context`;
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
# all four feature-gate on the py3.14 public
Doc `subint_fork` as blocked by CPython post-fork Empirical finding: the WIP `subint_fork_proc` scaffold landed in `cf0e3e6f` does *not* work on current CPython. The `fork()` syscall succeeds in the parent, but the CHILD aborts immediately during `PyOS_AfterFork_Child()` → `_PyInterpreterState_DeleteExceptMain()`, which gates on the current tstate belonging to the main interp — the child dies with `Fatal Python error: not main interpreter`. CPython devs acknowledge the fragility with an in-source comment (`// Ideally we could guarantee tstate is running main.`) but expose no user-facing hook to satisfy the precondition — so the strategy is structurally dead until upstream changes. Rather than delete the scaffold, reshape it into a documented dead-end so the next person with this idea lands on the reason rather than rediscovering the same CPython-level refusal. Deats, - Move `subint_fork_proc` out of `tractor.spawn._subint` into a new `tractor.spawn._subint_fork` dedicated module (153 LOC). Module + fn docstrings now describe the blockage directly; the fn body is trimmed to a `NotImplementedError` pointing at the analysis doc — no more dead-code `bootstrap` sketch bloating `_subint.py`. - `_spawn.py`: keep `'subint_fork'` in `SpawnMethodKey` + the `_methods` dispatch so `--spawn-backend=subint_fork` routes to a clean `NotImplementedError` rather than "invalid backend"; comment calls out the blockage. Collapse the duplicate py3.14 feature-gate in `try_set_start_method()` into a combined `case 'subint' | 'subint_fork':` arm. - New 337-line analysis: `ai/conc-anal/subint_fork_blocked_by_cpython_post_fork_issue.md`. Annotated walkthrough from the user-visible fatal error down to the specific `Modules/posixmodule.c` + `Python/pystate.c` source lines enforcing the refusal, plus an upstream-report draft. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 0f48ed2eb9541f14cf3646e760281c212e2eaa69)
2026-04-22 20:02:01 +00:00
# `concurrent.interpreters` wrapper (PEP 734). See
# `tractor.spawn._subint` for the detailed
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
# reasoning. `subint_fork` is blocked at the
# CPython level (raises `NotImplementedError`);
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
# `main_thread_forkserver` is the working
# variant-1 backend; `subint_forkserver` aliases
# to it today, reserved for the future variant-2
# subint-isolated-child runtime once upstream
# msgspec#1026 unblocks.
Add `'subint'` spawn backend scaffold (#379) Land the scaffolding for a future sub-interpreter (PEP 734 `concurrent.interpreters`) actor spawn backend per issue #379. The spawn flow itself is not yet implemented; `subint_proc()` raises a placeholder `NotImplementedError` pointing at the tracking issue — this commit only wires up the registry, the py-version gate, and the harness. Deats, - bump `pyproject.toml` `requires-python` to `>=3.12, <3.15` and list the `3.14` classifier — the new stdlib `concurrent.interpreters` module only ships on 3.14 - extend `SpawnMethodKey = Literal[..., 'subint']` - `try_set_start_method('subint')` grows a new `match` arm that feature-detects the stdlib module and raises `RuntimeError` with a clear banner on py<3.14 - `_methods` registers the new `subint_proc()` via the same bottom-of-module late-import pattern used for `._trio` / `._mp` Also, - new `tractor/spawn/_subint.py` — top-level `try: from concurrent import interpreters` guards `_has_subints: bool`; `subint_proc()` signature mirrors `trio_proc`/`mp_proc` so the Phase B.2 impl can drop in without touching the registry - re-add `import sys` to `_spawn.py` (needed for the py-version msg in the gate-error) - `_testing.pytest.pytest_configure` wraps `try_set_start_method()` in a `pytest.UsageError` handler so `--spawn-backend=subint` on py<3.14 prints a clean banner instead of a traceback (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit d318f1f8f4f6a056a8ca67d7f15e4e2a72507ea3) (MTF-only portion: kept tractor/spawn/_spawn.py tractor/spawn/_subint.py)
2026-06-16 00:04:25 +00:00
from ._subint import _has_subints
if not _has_subints:
raise RuntimeError(
f'Spawn method {key!r} requires Python 3.14+.\n'
f'(On py3.13 the private `_interpreters` C '
f'module exists but tractor\'s spawn flow '
f'wedges — see `tractor.spawn._subint` '
f'docstring for details.)\n'
Add `'subint'` spawn backend scaffold (#379) Land the scaffolding for a future sub-interpreter (PEP 734 `concurrent.interpreters`) actor spawn backend per issue #379. The spawn flow itself is not yet implemented; `subint_proc()` raises a placeholder `NotImplementedError` pointing at the tracking issue — this commit only wires up the registry, the py-version gate, and the harness. Deats, - bump `pyproject.toml` `requires-python` to `>=3.12, <3.15` and list the `3.14` classifier — the new stdlib `concurrent.interpreters` module only ships on 3.14 - extend `SpawnMethodKey = Literal[..., 'subint']` - `try_set_start_method('subint')` grows a new `match` arm that feature-detects the stdlib module and raises `RuntimeError` with a clear banner on py<3.14 - `_methods` registers the new `subint_proc()` via the same bottom-of-module late-import pattern used for `._trio` / `._mp` Also, - new `tractor/spawn/_subint.py` — top-level `try: from concurrent import interpreters` guards `_has_subints: bool`; `subint_proc()` signature mirrors `trio_proc`/`mp_proc` so the Phase B.2 impl can drop in without touching the registry - re-add `import sys` to `_spawn.py` (needed for the py-version msg in the gate-error) - `_testing.pytest.pytest_configure` wraps `try_set_start_method()` in a `pytest.UsageError` handler so `--spawn-backend=subint` on py<3.14 prints a clean banner instead of a traceback (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit d318f1f8f4f6a056a8ca67d7f15e4e2a72507ea3) (MTF-only portion: kept tractor/spawn/_spawn.py tractor/spawn/_subint.py)
2026-06-16 00:04:25 +00:00
f'Current runtime: {sys.version}'
)
_ctx = None
case _:
raise ValueError(
f'Spawn method `{key}` is invalid!\n'
f'Please choose one of {SpawnMethodKey}'
)
_spawn_method = key
return _ctx
2020-01-20 16:10:51 +00:00
async def exhaust_portal(
2021-11-20 18:01:22 +00:00
2020-01-20 16:10:51 +00:00
portal: Portal,
actor: Actor
2021-11-20 18:01:22 +00:00
2020-01-20 16:10:51 +00:00
) -> Any:
2021-11-20 18:01:22 +00:00
'''
Pull final result from portal (assuming it has one).
2020-01-20 16:10:51 +00:00
If the main task is an async generator do our best to consume
what's left of it.
2021-11-20 18:01:22 +00:00
'''
__tracebackhide__ = True
2020-01-20 16:10:51 +00:00
try:
log.debug(
Use `.aid.uid` to avoid deprecation warns I started getting annoyed by all the warnings from `pytest` during work on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid` accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the core runtime and IPC subsystems to avoid the noise. This also provides incentive to start the adjustment to all `.uid`-holding/tracking internal `dict`-tables/data-structures to instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow up shortly B) Deats, - `._context`: swap all `self._actor.uid`, `self.chan.uid`, and `portal.actor.uid` refs to `.aid.uid`; use `.aid.reprol()` for log/error formatting. - `._rpc`: same treatment for `actor.uid`, `chan.uid` in log msgs and cancel-scope handling; fix `str(err)` typo in `ContextCancelled` log. - `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx cache lookups, RPC `Start` msg, registration and cancel-request handling; improve ctxc log formatting. - `._spawn`: replace all `subactor.uid` with `.aid.uid` for child-proc tracking, IPC peer waiting, debug-lock acquisition, and nursery child dict ops. - `._supervise`: same for `subactor.uid` in cancel and portal-wait paths; use `actor.aid.uid` for error dict. - `._state`: fix `last.uid` -> `last.aid.uid` in `current_actor()` error msg. Also, - `._chan`: make `Channel.aid` a proper `@property` backed by `._aid` so we can add validation/typing later. - `.log`: use `current_actor().aid.uuid` instead of `.uid[1]` for actor-uid log field. - `.msg.types`: add TODO comment for `Start.aid` field conversion. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
f'Waiting on final result from {actor.aid.uid}'
)
# XXX: streams should never be reaped here since they should
# always be established and shutdown using a context manager api
final: Any = await portal.wait_for_result()
except (
Exception,
BaseExceptionGroup,
) as err:
# we reraise in the parent task via a ``BaseExceptionGroup``
2020-01-20 16:10:51 +00:00
return err
except trio.Cancelled as err:
# lol, of course we need this too ;P
# TODO: merge with above?
log.warning(
'Cancelled portal result waiter task:\n'
f'uid: {portal.channel.aid}\n'
f'error: {err}\n'
)
return err
2020-01-20 16:10:51 +00:00
else:
log.debug(
f'Returning final result from portal:\n'
f'uid: {portal.channel.aid}\n'
f'result: {final}\n'
)
2020-01-20 16:10:51 +00:00
return final
async def cancel_on_completion(
2021-11-20 18:01:22 +00:00
2020-01-20 16:10:51 +00:00
portal: Portal,
actor: Actor,
errors: dict[tuple[str, str], Exception],
2020-01-20 16:10:51 +00:00
) -> None:
2021-11-20 18:01:22 +00:00
'''
Cancel actor gracefully once its "main" portal's
2020-01-20 16:10:51 +00:00
result arrives.
Should only be called for actors spawned via the
`Portal.run_in_actor()` API.
=> and really this API will be deprecated and should be
re-implemented as a `.hilevel.one_shot_task_nursery()`..)
2021-11-20 18:01:22 +00:00
'''
# if this call errors we store the exception for later
# in ``errors`` which will be reraised inside
# an exception group and we still send out a cancel request
result: Any|Exception = await exhaust_portal(
portal,
actor,
)
if isinstance(result, Exception):
Use `.aid.uid` to avoid deprecation warns I started getting annoyed by all the warnings from `pytest` during work on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid` accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the core runtime and IPC subsystems to avoid the noise. This also provides incentive to start the adjustment to all `.uid`-holding/tracking internal `dict`-tables/data-structures to instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow up shortly B) Deats, - `._context`: swap all `self._actor.uid`, `self.chan.uid`, and `portal.actor.uid` refs to `.aid.uid`; use `.aid.reprol()` for log/error formatting. - `._rpc`: same treatment for `actor.uid`, `chan.uid` in log msgs and cancel-scope handling; fix `str(err)` typo in `ContextCancelled` log. - `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx cache lookups, RPC `Start` msg, registration and cancel-request handling; improve ctxc log formatting. - `._spawn`: replace all `subactor.uid` with `.aid.uid` for child-proc tracking, IPC peer waiting, debug-lock acquisition, and nursery child dict ops. - `._supervise`: same for `subactor.uid` in cancel and portal-wait paths; use `actor.aid.uid` for error dict. - `._state`: fix `last.uid` -> `last.aid.uid` in `current_actor()` error msg. Also, - `._chan`: make `Channel.aid` a proper `@property` backed by `._aid` so we can add validation/typing later. - `.log`: use `current_actor().aid.uuid` instead of `.uid[1]` for actor-uid log field. - `.msg.types`: add TODO comment for `Start.aid` field conversion. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
errors[actor.aid.uid]: Exception = result
log.cancel(
'Cancelling subactor runtime due to error:\n\n'
Use `.aid.uid` to avoid deprecation warns I started getting annoyed by all the warnings from `pytest` during work on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid` accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the core runtime and IPC subsystems to avoid the noise. This also provides incentive to start the adjustment to all `.uid`-holding/tracking internal `dict`-tables/data-structures to instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow up shortly B) Deats, - `._context`: swap all `self._actor.uid`, `self.chan.uid`, and `portal.actor.uid` refs to `.aid.uid`; use `.aid.reprol()` for log/error formatting. - `._rpc`: same treatment for `actor.uid`, `chan.uid` in log msgs and cancel-scope handling; fix `str(err)` typo in `ContextCancelled` log. - `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx cache lookups, RPC `Start` msg, registration and cancel-request handling; improve ctxc log formatting. - `._spawn`: replace all `subactor.uid` with `.aid.uid` for child-proc tracking, IPC peer waiting, debug-lock acquisition, and nursery child dict ops. - `._supervise`: same for `subactor.uid` in cancel and portal-wait paths; use `actor.aid.uid` for error dict. - `._state`: fix `last.uid` -> `last.aid.uid` in `current_actor()` error msg. Also, - `._chan`: make `Channel.aid` a proper `@property` backed by `._aid` so we can add validation/typing later. - `.log`: use `current_actor().aid.uuid` instead of `.uid[1]` for actor-uid log field. - `.msg.types`: add TODO comment for `Start.aid` field conversion. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
f'Portal.cancel_actor() => {portal.channel.aid}\n\n'
f'error: {result}\n'
)
else:
log.runtime(
'Cancelling subactor gracefully:\n\n'
Use `.aid.uid` to avoid deprecation warns I started getting annoyed by all the warnings from `pytest` during work on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid` accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the core runtime and IPC subsystems to avoid the noise. This also provides incentive to start the adjustment to all `.uid`-holding/tracking internal `dict`-tables/data-structures to instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow up shortly B) Deats, - `._context`: swap all `self._actor.uid`, `self.chan.uid`, and `portal.actor.uid` refs to `.aid.uid`; use `.aid.reprol()` for log/error formatting. - `._rpc`: same treatment for `actor.uid`, `chan.uid` in log msgs and cancel-scope handling; fix `str(err)` typo in `ContextCancelled` log. - `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx cache lookups, RPC `Start` msg, registration and cancel-request handling; improve ctxc log formatting. - `._spawn`: replace all `subactor.uid` with `.aid.uid` for child-proc tracking, IPC peer waiting, debug-lock acquisition, and nursery child dict ops. - `._supervise`: same for `subactor.uid` in cancel and portal-wait paths; use `actor.aid.uid` for error dict. - `._state`: fix `last.uid` -> `last.aid.uid` in `current_actor()` error msg. Also, - `._chan`: make `Channel.aid` a proper `@property` backed by `._aid` so we can add validation/typing later. - `.log`: use `current_actor().aid.uuid` instead of `.uid[1]` for actor-uid log field. - `.msg.types`: add TODO comment for `Start.aid` field conversion. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
f'Portal.cancel_actor() => {portal.channel.aid}\n\n'
f'result: {result}\n'
)
2020-01-20 16:10:51 +00:00
# cancel the process now that we have a final result
await portal.cancel_actor()
2020-01-20 16:10:51 +00:00
async def hard_kill(
proc: trio.Process,
terminate_after: int = 1.6,
# NOTE: for mucking with `.pause()`-ing inside the runtime
# whilst also hacking on it XD
# terminate_after: int = 99999,
*,
# Subactor's bind addresses + subactor record, used
# for post-SIGKILL UDS sockpath cleanup. Optional for
# legacy callers; new call sites should pass at least
# `subactor` (which lets us reconstruct the sock path
# from `aid.name + proc.pid` when `bind_addrs` is
# empty/self-assigned). See `._reap.unlink_uds_bind_addrs()`.
bind_addrs: list[UnwrappedAddress] | None = None,
subactor: Actor | None = None,
) -> None:
2023-12-11 23:17:42 +00:00
'''
Un-gracefully terminate an OS level `trio.Process` after timeout.
Used in 2 main cases:
- "unknown remote runtime state": a hanging/stalled actor that
isn't responding after sending a (graceful) runtime cancel
request via an IPC msg.
- "cancelled during spawn": a process who's actor runtime was
cancelled before full startup completed (such that
cancel-request-handling machinery was never fully
initialized) and thus a "cancel request msg" is never going
to be handled.
'''
log.cancel(
'Terminating sub-proc\n'
f'>x)\n'
f' |_{proc}\n'
)
# NOTE: this timeout used to do nothing since we were shielding
# the ``.wait()`` inside ``new_proc()`` which will pretty much
# never release until the process exits, now it acts as
# a hard-kill time ultimatum.
with trio.move_on_after(terminate_after) as cs:
# NOTE: code below was copied verbatim from the now deprecated
# (in 0.20.0) ``trio._subrocess.Process.aclose()``, orig doc
# string:
#
# Close any pipes we have to the process (both input and output)
# and wait for it to exit. If cancelled, kills the process and
# waits for it to finish exiting before propagating the
# cancellation.
2023-12-11 23:17:42 +00:00
#
# This code was originally triggred by ``proc.__aexit__()``
# but now must be called manually.
with trio.CancelScope(shield=True):
if proc.stdin is not None:
await proc.stdin.aclose()
if proc.stdout is not None:
await proc.stdout.aclose()
if proc.stderr is not None:
await proc.stderr.aclose()
try:
await proc.wait()
finally:
if proc.returncode is None:
proc.kill()
with trio.CancelScope(shield=True):
await proc.wait()
2023-12-11 23:17:42 +00:00
# XXX NOTE XXX: zombie squad dispatch:
# (should ideally never, but) If we do get here it means
# graceful termination of a process failed and we need to
# resort to OS level signalling to interrupt and cancel the
# (presumably stalled or hung) actor. Since we never allow
# zombies (as a feature) we ask the OS to do send in the
# removal swad as the last resort.
if cs.cancelled_caught:
# TODO? attempt at intermediary-rent-sub
# with child in debug lock?
# |_https://github.com/goodboy/tractor/issues/320
#
# if not is_root_process():
# log.warning(
# 'Attempting to acquire debug-REPL-lock before zombie reap!'
# )
# with trio.CancelScope(shield=True):
# async with debug.acquire_debug_lock(
Use `.aid.uid` to avoid deprecation warns I started getting annoyed by all the warnings from `pytest` during work on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid` accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the core runtime and IPC subsystems to avoid the noise. This also provides incentive to start the adjustment to all `.uid`-holding/tracking internal `dict`-tables/data-structures to instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow up shortly B) Deats, - `._context`: swap all `self._actor.uid`, `self.chan.uid`, and `portal.actor.uid` refs to `.aid.uid`; use `.aid.reprol()` for log/error formatting. - `._rpc`: same treatment for `actor.uid`, `chan.uid` in log msgs and cancel-scope handling; fix `str(err)` typo in `ContextCancelled` log. - `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx cache lookups, RPC `Start` msg, registration and cancel-request handling; improve ctxc log formatting. - `._spawn`: replace all `subactor.uid` with `.aid.uid` for child-proc tracking, IPC peer waiting, debug-lock acquisition, and nursery child dict ops. - `._supervise`: same for `subactor.uid` in cancel and portal-wait paths; use `actor.aid.uid` for error dict. - `._state`: fix `last.uid` -> `last.aid.uid` in `current_actor()` error msg. Also, - `._chan`: make `Channel.aid` a proper `@property` backed by `._aid` so we can add validation/typing later. - `.log`: use `current_actor().aid.uuid` instead of `.uid[1]` for actor-uid log field. - `.msg.types`: add TODO comment for `Start.aid` field conversion. (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
# subactor_uid=current_actor().aid.uid,
# ) as _ctx:
# log.warning(
# 'Acquired debug lock, child ready to be killed ??\n'
# )
# TODO: toss in the skynet-logo face as ascii art?
log.critical(
# 'Well, the #ZOMBIE_LORD_IS_HERE# to collect\n'
'#T-800 deployed to collect zombie B0\n'
f'>x)\n'
f' |_{proc}\n'
)
proc.kill()
# Post-mortem UDS sockpath cleanup. SIGKILL bypassed
# the subactor's normal `os.unlink(addr.sockpath)` in
# `_serve_ipc_eps`'s `finally:`; the parent has the
# bind addrs (or can reconstruct from name + pid) so
# we do it here. Runs UNCONDITIONALLY (graceful-exit
# case is a no-op via `FileNotFoundError` skip in the
# helper) so the cleanup also covers the "cancelled
# during spawn" path where the subactor never reached
# its IPC server finally block.
unlink_uds_bind_addrs(
proc,
bind_addrs=bind_addrs,
subactor=subactor,
)
async def soft_kill(
2021-12-02 17:34:27 +00:00
proc: ProcessType,
wait_func: Callable[
2021-12-02 17:34:27 +00:00
[ProcessType],
Awaitable,
],
portal: Portal,
) -> None:
2023-12-11 23:17:42 +00:00
'''
Wait for proc termination but **don't yet** teardown
std-streams since it will clobber any ongoing pdb REPL
session.
This is our "soft"/graceful, and thus itself also cancellable,
join/reap on an actor-runtime-in-process shutdown; it is
**not** the same as a "hard kill" via an OS signal (for that
see `.hard_kill()`).
2023-12-11 23:17:42 +00:00
'''
chan: Channel = portal.channel
peer_aid: msgtypes.Aid = chan.aid
try:
log.cancel(
f'Soft killing sub-actor via portal request\n'
f'\n'
f'c)=> {peer_aid.reprol()}@[{chan.maddr}]\n'
f' |_{proc}\n'
)
# wait on sub-proc to signal termination
await wait_func(proc)
except trio.Cancelled:
with trio.CancelScope(shield=True):
await debug.maybe_wait_for_debugger(
child_in_debug=_runtime_vars.get(
'_debug_mode', False
),
header_msg=(
'Delaying `soft_kill()` subproc reaper while debugger locked..\n'
),
# TODO: need a diff value then default?
# poll_steps=9999999,
)
# if cancelled during a soft wait, cancel the child
# actor before entering the hard reap sequence
# below. This means we try to do a graceful teardown
# via sending a cancel message before getting out
# zombie killing tools.
async with trio.open_nursery() as n:
n.cancel_scope.shield = True
async def cancel_on_proc_deth():
'''
"Cancel-the-cancel" request: if we detect that the
underlying sub-process exited prior to
a `Portal.cancel_actor()` call completing .
'''
await wait_func(proc)
n.cancel_scope.cancel()
2023-12-11 23:17:42 +00:00
# start a task to wait on the termination of the
# process by itself waiting on a (caller provided) wait
# function which should unblock when the target process
# has terminated.
n.start_soon(cancel_on_proc_deth)
2023-12-11 23:17:42 +00:00
# send the actor-runtime a cancel request.
await portal.cancel_actor()
if proc.poll() is None: # type: ignore
log.warning(
'Subactor still alive after cancel request?\n\n'
f'uid: {peer_aid}\n'
f'|_{proc}\n'
)
n.cancel_scope.cancel()
raise
2019-11-26 14:23:37 +00:00
async def new_proc(
name: str,
actor_nursery: ActorNursery,
subactor: Actor,
errors: dict[tuple[str, str], Exception],
# passed through to actor main
bind_addrs: list[UnwrappedAddress],
parent_addr: UnwrappedAddress,
_runtime_vars: dict[str, Any], # serialized and sent to _child
*,
infect_asyncio: bool = False,
2025-03-12 19:13:40 +00:00
task_status: TaskStatus[Portal] = trio.TASK_STATUS_IGNORED,
proc_kwargs: dict[str, any] = {}
) -> None:
# lookup backend spawning target
target: Callable = _methods[_spawn_method]
# mark the new actor with the global spawn method
subactor._spawn_method = _spawn_method
await target(
name,
actor_nursery,
subactor,
errors,
Init-support for "multi homed" transports Since we'd like to eventually allow a diverse set of transport (protocol) methods and stacks, and a multi-peer discovery system for distributed actor-tree applications, this reworks all runtime internals to support multi-homing for any given tree on a logical host. In other words any actor can now bind its transport server (currently only unsecured TCP + `msgspec`) to more then one address available in its (linux) network namespace. Further, registry actors (now dubbed "registars" instead of "arbiters") can also similarly bind to multiple network addresses and provide discovery services to remote actors via multiple addresses which can now be provided at runtime startup. Deats: - adjust `._runtime` internals to use a `list[tuple[str, int]]` (and thus pluralized) socket address sequence where applicable for transport server socket binds, now exposed via `Actor.accept_addrs`: - `Actor.__init__()` now takes a `registry_addrs: list`. - `Actor.is_arbiter` -> `.is_registrar`. - `._arb_addr` -> `._reg_addrs: list[tuple]`. - always reg and de-reg from all registrars in `async_main()`. - only set the global runtime var `'_root_mailbox'` to the loopback address since normally all in-tree processes should have access to it, right? - `._serve_forever()` task now takes `listen_sockaddrs: list[tuple]` - make `open_root_actor()` take a `registry_addrs: list[tuple[str, int]]` and defaults when not passed. - change `ActorNursery.start_..()` methods take `bind_addrs: list` and pass down through the spawning layer(s) via the parent-seed-msg. - generalize all `._discovery()` APIs to accept `registry_addrs`-like inputs and move all relevant subsystems to adopt the "registry" style naming instead of "arbiter": - make `find_actor()` support batched concurrent portal queries over all provided input addresses using `.trionics.gather_contexts()` Bo - syntax: move to using `async with <tuples>` 3.9+ style chained @acms. - a general modernization of the code to a python 3.9+ style. - start deprecation and change to "registry" naming / semantics: - `._discovery.get_arbiter()` -> `.get_registry()`
2023-09-27 19:19:30 +00:00
bind_addrs,
parent_addr,
_runtime_vars, # run time vars
infect_asyncio=infect_asyncio,
task_status=task_status,
2025-03-12 19:13:40 +00:00
proc_kwargs=proc_kwargs
)
# NOTE: bottom-of-module to avoid a circular import since the
# backend submodules pull `cancel_on_completion`/`soft_kill`/
# `hard_kill`/`proc_waiter` from this module.
from ._trio import trio_proc
from ._mp import mp_proc
Add `'subint'` spawn backend scaffold (#379) Land the scaffolding for a future sub-interpreter (PEP 734 `concurrent.interpreters`) actor spawn backend per issue #379. The spawn flow itself is not yet implemented; `subint_proc()` raises a placeholder `NotImplementedError` pointing at the tracking issue — this commit only wires up the registry, the py-version gate, and the harness. Deats, - bump `pyproject.toml` `requires-python` to `>=3.12, <3.15` and list the `3.14` classifier — the new stdlib `concurrent.interpreters` module only ships on 3.14 - extend `SpawnMethodKey = Literal[..., 'subint']` - `try_set_start_method('subint')` grows a new `match` arm that feature-detects the stdlib module and raises `RuntimeError` with a clear banner on py<3.14 - `_methods` registers the new `subint_proc()` via the same bottom-of-module late-import pattern used for `._trio` / `._mp` Also, - new `tractor/spawn/_subint.py` — top-level `try: from concurrent import interpreters` guards `_has_subints: bool`; `subint_proc()` signature mirrors `trio_proc`/`mp_proc` so the Phase B.2 impl can drop in without touching the registry - re-add `import sys` to `_spawn.py` (needed for the py-version msg in the gate-error) - `_testing.pytest.pytest_configure` wraps `try_set_start_method()` in a `pytest.UsageError` handler so `--spawn-backend=subint` on py<3.14 prints a clean banner instead of a traceback (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit d318f1f8f4f6a056a8ca67d7f15e4e2a72507ea3) (MTF-only portion: kept tractor/spawn/_spawn.py tractor/spawn/_subint.py)
2026-06-16 00:04:25 +00:00
from ._subint import subint_proc
Doc `subint_fork` as blocked by CPython post-fork Empirical finding: the WIP `subint_fork_proc` scaffold landed in `cf0e3e6f` does *not* work on current CPython. The `fork()` syscall succeeds in the parent, but the CHILD aborts immediately during `PyOS_AfterFork_Child()` → `_PyInterpreterState_DeleteExceptMain()`, which gates on the current tstate belonging to the main interp — the child dies with `Fatal Python error: not main interpreter`. CPython devs acknowledge the fragility with an in-source comment (`// Ideally we could guarantee tstate is running main.`) but expose no user-facing hook to satisfy the precondition — so the strategy is structurally dead until upstream changes. Rather than delete the scaffold, reshape it into a documented dead-end so the next person with this idea lands on the reason rather than rediscovering the same CPython-level refusal. Deats, - Move `subint_fork_proc` out of `tractor.spawn._subint` into a new `tractor.spawn._subint_fork` dedicated module (153 LOC). Module + fn docstrings now describe the blockage directly; the fn body is trimmed to a `NotImplementedError` pointing at the analysis doc — no more dead-code `bootstrap` sketch bloating `_subint.py`. - `_spawn.py`: keep `'subint_fork'` in `SpawnMethodKey` + the `_methods` dispatch so `--spawn-backend=subint_fork` routes to a clean `NotImplementedError` rather than "invalid backend"; comment calls out the blockage. Collapse the duplicate py3.14 feature-gate in `try_set_start_method()` into a combined `case 'subint' | 'subint_fork':` arm. - New 337-line analysis: `ai/conc-anal/subint_fork_blocked_by_cpython_post_fork_issue.md`. Annotated walkthrough from the user-visible fatal error down to the specific `Modules/posixmodule.c` + `Python/pystate.c` source lines enforcing the refusal, plus an upstream-report draft. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 0f48ed2eb9541f14cf3646e760281c212e2eaa69)
2026-04-22 20:02:01 +00:00
from ._subint_fork import subint_fork_proc
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
from ._main_thread_forkserver import main_thread_forkserver_proc
from ._subint_forkserver import subint_forkserver_proc
# proc spawning backend target map
_methods: dict[SpawnMethodKey, Callable] = {
'trio': trio_proc,
'mp_spawn': mp_proc,
'mp_forkserver': mp_proc,
Add `'subint'` spawn backend scaffold (#379) Land the scaffolding for a future sub-interpreter (PEP 734 `concurrent.interpreters`) actor spawn backend per issue #379. The spawn flow itself is not yet implemented; `subint_proc()` raises a placeholder `NotImplementedError` pointing at the tracking issue — this commit only wires up the registry, the py-version gate, and the harness. Deats, - bump `pyproject.toml` `requires-python` to `>=3.12, <3.15` and list the `3.14` classifier — the new stdlib `concurrent.interpreters` module only ships on 3.14 - extend `SpawnMethodKey = Literal[..., 'subint']` - `try_set_start_method('subint')` grows a new `match` arm that feature-detects the stdlib module and raises `RuntimeError` with a clear banner on py<3.14 - `_methods` registers the new `subint_proc()` via the same bottom-of-module late-import pattern used for `._trio` / `._mp` Also, - new `tractor/spawn/_subint.py` — top-level `try: from concurrent import interpreters` guards `_has_subints: bool`; `subint_proc()` signature mirrors `trio_proc`/`mp_proc` so the Phase B.2 impl can drop in without touching the registry - re-add `import sys` to `_spawn.py` (needed for the py-version msg in the gate-error) - `_testing.pytest.pytest_configure` wraps `try_set_start_method()` in a `pytest.UsageError` handler so `--spawn-backend=subint` on py<3.14 prints a clean banner instead of a traceback (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit d318f1f8f4f6a056a8ca67d7f15e4e2a72507ea3) (MTF-only portion: kept tractor/spawn/_spawn.py tractor/spawn/_subint.py)
2026-06-16 00:04:25 +00:00
'subint': subint_proc,
Doc `subint_fork` as blocked by CPython post-fork Empirical finding: the WIP `subint_fork_proc` scaffold landed in `cf0e3e6f` does *not* work on current CPython. The `fork()` syscall succeeds in the parent, but the CHILD aborts immediately during `PyOS_AfterFork_Child()` → `_PyInterpreterState_DeleteExceptMain()`, which gates on the current tstate belonging to the main interp — the child dies with `Fatal Python error: not main interpreter`. CPython devs acknowledge the fragility with an in-source comment (`// Ideally we could guarantee tstate is running main.`) but expose no user-facing hook to satisfy the precondition — so the strategy is structurally dead until upstream changes. Rather than delete the scaffold, reshape it into a documented dead-end so the next person with this idea lands on the reason rather than rediscovering the same CPython-level refusal. Deats, - Move `subint_fork_proc` out of `tractor.spawn._subint` into a new `tractor.spawn._subint_fork` dedicated module (153 LOC). Module + fn docstrings now describe the blockage directly; the fn body is trimmed to a `NotImplementedError` pointing at the analysis doc — no more dead-code `bootstrap` sketch bloating `_subint.py`. - `_spawn.py`: keep `'subint_fork'` in `SpawnMethodKey` + the `_methods` dispatch so `--spawn-backend=subint_fork` routes to a clean `NotImplementedError` rather than "invalid backend"; comment calls out the blockage. Collapse the duplicate py3.14 feature-gate in `try_set_start_method()` into a combined `case 'subint' | 'subint_fork':` arm. - New 337-line analysis: `ai/conc-anal/subint_fork_blocked_by_cpython_post_fork_issue.md`. Annotated walkthrough from the user-visible fatal error down to the specific `Modules/posixmodule.c` + `Python/pystate.c` source lines enforcing the refusal, plus an upstream-report draft. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 0f48ed2eb9541f14cf3646e760281c212e2eaa69)
2026-04-22 20:02:01 +00:00
# blocked at CPython level — see `_subint_fork.py` +
# `ai/conc-anal/subint_fork_blocked_by_cpython_post_fork_issue.md`.
# Kept here so `--spawn-backend=subint_fork` routes to a
# clean `NotImplementedError` with pointer to the analysis,
# rather than an "invalid backend" error.
'subint_fork': subint_fork_proc,
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
# Variant-1 (working today): fork from a regular main-interp
# worker thread, child runs trio on its own main interp.
# Validated by
# `ai/conc-anal/subint_fork_from_main_thread_smoketest.py`.
# See `tractor.spawn._main_thread_forkserver`.
'main_thread_forkserver': main_thread_forkserver_proc,
# Variant-2 (future, reserved): same fork machinery but
# child enters a sub-interpreter to host its `trio.run()`
# — gated on jcrist/msgspec#1026 unblocking PEP 684
# isolated-mode subints. Today the stub raises
# `NotImplementedError` pointing at the variant-1 backend
# + upstream blocker. See
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
# `tractor.spawn._subint_forkserver`.
'subint_forkserver': subint_forkserver_proc,
}