Doc the `_interpreters` private-API choice in `_subint`

Expand the comment block above the `_interpreters`
import explaining *why* we use the private C mod
over `concurrent.interpreters`: the public API only
exposes PEP 734's `'isolated'` config which breaks
`msgspec` (missing PEP 684 slot). Add reference
links to PEP 734, PEP 684, cpython sources, and
the msgspec upstream tracker (jcrist/msgspec#563).

Also,
- update error msgs in both `_spawn.py` and
  `_subint.py` to say "3.13+" (matching the actual
  `_interpreters` availability) instead of "3.14+".
- tweak the mod docstring to reflect py3.13+
  availability via the private C module.

Review: PR #444 (copilot-pull-request-reviewer)
https://github.com/goodboy/tractor/pull/444

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
subint_forkserver_backend
Gud Boi 2026-04-17 20:10:46 -04:00
parent 03bf2b931e
commit 8a8d01e076
2 changed files with 55 additions and 20 deletions

View File

@ -116,12 +116,17 @@ def try_set_start_method(
_ctx = None _ctx = None
case 'subint': case 'subint':
# subints need no `mp.context`; feature-gate 3.14+ # subints need no `mp.context`; feature-gate on the
# private `_interpreters` C module (available py3.13+
# via cpython's internal stdlib — predates the PEP 734
# public wrapper which only lands in py3.14).
from ._subint import _has_subints from ._subint import _has_subints
if not _has_subints: if not _has_subints:
raise RuntimeError( raise RuntimeError(
f'Spawn method {key!r} requires Python 3.14+ ' f'Spawn method {key!r} requires Python 3.13+ '
f'(stdlib `concurrent.interpreters`, PEP 734).\n' f'(private stdlib `_interpreters` C module; '
f'the public `concurrent.interpreters` wrapper '
f'lands in py3.14).\n'
f'Current runtime: {sys.version}' f'Current runtime: {sys.version}'
) )
_ctx = None _ctx = None

View File

@ -25,9 +25,13 @@ IPC-based actor boundary.
Availability Availability
------------ ------------
Requires Python 3.14+ for the stdlib `concurrent.interpreters` Runs on py3.13+ via the *private* stdlib `_interpreters` C
module. On older runtimes the module still imports (so the module (which predates the py3.14 public
registry stays introspectable) but `subint_proc()` raises. `concurrent.interpreters` stdlib wrapper). See the comment
above the `_interpreters` import below for the trade-offs
driving the private-API choice. On older runtimes the
module still imports (so the registry stays
introspectable) but `subint_proc()` raises.
''' '''
from __future__ import annotations from __future__ import annotations
@ -43,18 +47,43 @@ from trio import TaskStatus
# NOTE: we reach into the *private* `_interpreters` C module # NOTE: we reach into the *private* `_interpreters` C module
# rather than using the nice `concurrent.interpreters` public # rather than `concurrent.interpreters`' public API because the
# API because the latter only exposes the `'isolated'` subint # public API only exposes PEP 734's `'isolated'` config
# config (PEP 684, per-interp GIL). Under that config, any C # (per-interp GIL). Under `'isolated'`, any C extension missing
# extension lacking the `Py_mod_multiple_interpreters` slot # the `Py_mod_multiple_interpreters` slot (PEP 684) refuses to
# refuses to import — which includes `msgspec` (used all over # import; in our stack that's `msgspec` — which tractor uses
# tractor's IPC layer) as of 0.19.x. Dropping to the `'legacy'` # pervasively in the IPC layer — so isolated-mode subints can't
# config keeps the main GIL + lets existing C extensions load # finish booting the sub-actor's `trio.run()`. msgspec PEP 684
# normally while preserving the state-isolation we actually # support is open upstream at jcrist/msgspec#563.
# need for the actor model (separate `sys.modules`, `__main__`, #
# globals). Once msgspec (and similar deps) opt-in to PEP 684 # Dropping to the `'legacy'` config keeps the main GIL + lets
# we can migrate to the public `interpreters.create()` API and # existing C extensions load normally while preserving the
# pick up per-interp-GIL parallelism for free. # state isolation we actually care about for the actor model
# (separate `sys.modules` / `__main__` / globals). Side win:
# the private `_interpreters` module has shipped since py3.13
# (it predates the PEP 734 stdlib landing), so the `subint`
# backend can run on py3.13+ despite `concurrent.interpreters`
# itself being 3.14+.
#
# Migration path: when msgspec (jcrist/msgspec#563) and any
# other PEP 684-holdout C deps opt-in, we can switch to the
# public `concurrent.interpreters.create()` API (isolated
# mode) and pick up per-interp-GIL parallelism for free.
#
# References:
# - PEP 734 (`concurrent.interpreters` public API):
# https://peps.python.org/pep-0734/
# - PEP 684 (per-interpreter GIL / `Py_mod_multiple_interpreters`):
# https://peps.python.org/pep-0684/
# - stdlib docs (3.14+):
# https://docs.python.org/3.14/library/concurrent.interpreters.html
# - CPython public wrapper source (`Lib/concurrent/interpreters/`):
# https://github.com/python/cpython/tree/main/Lib/concurrent/interpreters
# - CPython private C ext source
# (`Modules/_interpretersmodule.c`):
# https://github.com/python/cpython/blob/main/Modules/_interpretersmodule.c
# - msgspec PEP 684 upstream tracker:
# https://github.com/jcrist/msgspec/issues/563
try: try:
import _interpreters # type: ignore import _interpreters # type: ignore
_has_subints: bool = True _has_subints: bool = True
@ -123,8 +152,9 @@ async def subint_proc(
''' '''
if not _has_subints: if not _has_subints:
raise RuntimeError( raise RuntimeError(
f'The {"subint"!r} spawn backend requires Python 3.14+ ' f'The {"subint"!r} spawn backend requires Python 3.13+ '
f'(stdlib `concurrent.interpreters`, PEP 734).\n' f'(private stdlib `_interpreters` C module; the public '
f'`concurrent.interpreters` wrapper lands in py3.14).\n'
f'Current runtime: {sys.version}' f'Current runtime: {sys.version}'
) )