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-codesubint_spawner_backend
parent
3773ad8b77
commit
7cee62ce42
|
|
@ -9,7 +9,7 @@ name = "tractor"
|
||||||
version = "0.1.0a6dev0"
|
version = "0.1.0a6dev0"
|
||||||
description = 'structured concurrent `trio`-"actors"'
|
description = 'structured concurrent `trio`-"actors"'
|
||||||
authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }]
|
authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }]
|
||||||
requires-python = ">=3.12, <3.14"
|
requires-python = ">=3.12, <3.15"
|
||||||
readme = "docs/README.rst"
|
readme = "docs/README.rst"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
keywords = [
|
keywords = [
|
||||||
|
|
@ -31,6 +31,7 @@ classifiers = [
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
"Programming Language :: Python :: 3.13",
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Programming Language :: Python :: 3.14",
|
||||||
"Topic :: System :: Distributed Computing",
|
"Topic :: System :: Distributed Computing",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,13 @@ def pytest_addoption(
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
backend = config.option.spawn_backend
|
backend = config.option.spawn_backend
|
||||||
from tractor.spawn._spawn import try_set_start_method
|
from tractor.spawn._spawn import try_set_start_method
|
||||||
|
try:
|
||||||
try_set_start_method(backend)
|
try_set_start_method(backend)
|
||||||
|
except RuntimeError as err:
|
||||||
|
# e.g. `--spawn-backend=subint` on Python < 3.14 — turn the
|
||||||
|
# runtime gate error into a clean pytest usage error so the
|
||||||
|
# suite exits with a helpful banner instead of a traceback.
|
||||||
|
raise pytest.UsageError(str(err)) from err
|
||||||
|
|
||||||
# register custom marks to avoid warnings see,
|
# register custom marks to avoid warnings see,
|
||||||
# https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers
|
# https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ over multiple backends.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
import platform
|
import platform
|
||||||
|
import sys
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
|
|
@ -61,6 +62,7 @@ SpawnMethodKey = Literal[
|
||||||
'trio', # supported on all platforms
|
'trio', # supported on all platforms
|
||||||
'mp_spawn',
|
'mp_spawn',
|
||||||
'mp_forkserver', # posix only
|
'mp_forkserver', # posix only
|
||||||
|
'subint', # py3.14+ via `concurrent.interpreters` (PEP 734)
|
||||||
]
|
]
|
||||||
_spawn_method: SpawnMethodKey = 'trio'
|
_spawn_method: SpawnMethodKey = 'trio'
|
||||||
|
|
||||||
|
|
@ -113,6 +115,17 @@ def try_set_start_method(
|
||||||
case 'trio':
|
case 'trio':
|
||||||
_ctx = None
|
_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 _:
|
case _:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Spawn method `{key}` is invalid!\n'
|
f'Spawn method `{key}` is invalid!\n'
|
||||||
|
|
@ -437,6 +450,7 @@ async def new_proc(
|
||||||
# `hard_kill`/`proc_waiter` from this module.
|
# `hard_kill`/`proc_waiter` from this module.
|
||||||
from ._trio import trio_proc
|
from ._trio import trio_proc
|
||||||
from ._mp import mp_proc
|
from ._mp import mp_proc
|
||||||
|
from ._subint import subint_proc
|
||||||
|
|
||||||
|
|
||||||
# proc spawning backend target map
|
# proc spawning backend target map
|
||||||
|
|
@ -444,4 +458,5 @@ _methods: dict[SpawnMethodKey, Callable] = {
|
||||||
'trio': trio_proc,
|
'trio': trio_proc,
|
||||||
'mp_spawn': mp_proc,
|
'mp_spawn': mp_proc,
|
||||||
'mp_forkserver': mp_proc,
|
'mp_forkserver': mp_proc,
|
||||||
|
'subint': subint_proc,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
'''
|
||||||
|
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'
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue