From 2675906398470e899117d7bfdd453e93ae89100c Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 15 Jun 2026 20:04:25 -0400 Subject: [PATCH] Add `'subint'` spawn backend scaffold (#379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- tractor/spawn/_spawn.py | 15 ++++++ tractor/spawn/_subint.py | 100 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 tractor/spawn/_subint.py diff --git a/tractor/spawn/_spawn.py b/tractor/spawn/_spawn.py index c0218c30..9e34db2e 100644 --- a/tractor/spawn/_spawn.py +++ b/tractor/spawn/_spawn.py @@ -22,6 +22,7 @@ over multiple backends. from __future__ import annotations import multiprocessing as mp import platform +import sys from typing import ( Any, Awaitable, @@ -64,6 +65,7 @@ SpawnMethodKey = Literal[ 'trio', # supported on all platforms 'mp_spawn', 'mp_forkserver', # posix only + 'subint', # py3.14+ via `concurrent.interpreters` (PEP 734) ] _spawn_method: SpawnMethodKey = 'trio' @@ -116,6 +118,17 @@ def try_set_start_method( case 'trio': _ctx = None + case 'subint': + # subints need no `mp.context`; feature-gate 3.14+ + from ._subint import _has_subints + if not _has_subints: + raise RuntimeError( + f'Spawn method {key!r} requires Python 3.14+ ' + f'(stdlib `concurrent.interpreters`, PEP 734).\n' + f'Current runtime: {sys.version}' + ) + _ctx = None + case _: raise ValueError( f'Spawn method `{key}` is invalid!\n' @@ -465,6 +478,7 @@ async def new_proc( # `hard_kill`/`proc_waiter` from this module. from ._trio import trio_proc from ._mp import mp_proc +from ._subint import subint_proc # proc spawning backend target map @@ -472,4 +486,5 @@ _methods: dict[SpawnMethodKey, Callable] = { 'trio': trio_proc, 'mp_spawn': mp_proc, 'mp_forkserver': mp_proc, + 'subint': subint_proc, } diff --git a/tractor/spawn/_subint.py b/tractor/spawn/_subint.py new file mode 100644 index 00000000..09793496 --- /dev/null +++ b/tractor/spawn/_subint.py @@ -0,0 +1,100 @@ +# 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 . + +''' +Sub-interpreter (`subint`) actor spawning backend. + +Spawns each sub-actor as a CPython PEP 734 sub-interpreter +(`concurrent.interpreters.Interpreter`) — same-process state +isolation with faster start-up than an OS subproc, while +preserving tractor's IPC-based actor boundaries. + +Availability +------------ +Requires Python 3.14+ for the stdlib `concurrent.interpreters` +module. On older runtimes the module still imports (so the +registry stays introspectable) but `subint_proc()` raises. + +Status +------ +SCAFFOLDING STUB — `subint_proc()` is **not yet implemented**. +The real impl lands in Phase B.2 (see issue #379). + +''' +from __future__ import annotations +import sys +from typing import ( + Any, + TYPE_CHECKING, +) + +import trio +from trio import TaskStatus + + +try: + from concurrent import interpreters as _interpreters # type: ignore + _has_subints: bool = True +except ImportError: + _interpreters = None # type: ignore + _has_subints: bool = False + + +if TYPE_CHECKING: + from tractor.discovery._addr import UnwrappedAddress + from tractor.runtime._portal import Portal + from tractor.runtime._runtime import Actor + from tractor.runtime._supervise import ActorNursery + + +async def subint_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, + task_status: TaskStatus[Portal] = trio.TASK_STATUS_IGNORED, + proc_kwargs: dict[str, any] = {} + +) -> None: + ''' + Create a new sub-actor hosted inside a PEP 734 + sub-interpreter running in a dedicated OS thread, + reusing tractor's existing UDS/TCP IPC handshake + for parent<->child channel setup. + + NOT YET IMPLEMENTED — placeholder stub pending the + Phase B.2 impl. + + ''' + if not _has_subints: + raise RuntimeError( + f'The {"subint"!r} spawn backend requires Python 3.14+ ' + f'(stdlib `concurrent.interpreters`, PEP 734).\n' + f'Current runtime: {sys.version}' + ) + + raise NotImplementedError( + 'The `subint` spawn backend scaffolding is in place but ' + 'the spawn-flow itself is not yet implemented.\n' + 'Tracking: https://github.com/goodboy/tractor/issues/379' + )