Raise `subint` floor to py3.14 and split dep-groups

The private `_interpreters` C module ships since 3.13, but that vintage
wedges under our `threading.Thread` + multi-trio usage pattern
—> `_interpreters.exec()` silently never makes progress. 3.14 fixes it.
So gate on the presence of the public `concurrent.interpreters` wrapper
(3.14+ only) even tho we still call into the private module at runtime.

Deats,
- `try_set_start_method('subint')` error msg + `_subint` module
  docstring/comments rewritten to document the 3.14 floor and why 3.13
  can't work.
- `_subint._has_subints` gate now imports `concurrent.interpreters` (not
  `_interpreters`) as the version sentinel.

Also, reshuffle `pyproject.toml` deps into
per-python-version `[tool.uv.dependency-groups]`:
- `subints` group: `msgspec>=0.21.0`, py>=3.14
- `eventfd` group: `cffi>=1.17.1`, py>=3.13,<3.14
- `sync_pause` group: `greenback`, py>=3.13,<3.14
  (was in `devx`; moved out bc no 3.14 yet)

Bump top-level `msgspec>=0.20.0` too.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code

(cherry picked from commit 34d9d482e4)
(MTF-only portion: kept tractor/spawn/_spawn.py tractor/spawn/_subint.py)
Gud Boi 2026-06-15 20:04:25 -04:00
parent a44329214d
commit 76d1a22a0e
2 changed files with 52 additions and 29 deletions

View File

@ -120,16 +120,21 @@ def try_set_start_method(
case 'subint': case 'subint':
# subints need no `mp.context`; feature-gate on the # subints need no `mp.context`; feature-gate on the
# private `_interpreters` C module (available py3.13+ # py3.14 public `concurrent.interpreters` wrapper
# via cpython's internal stdlib — predates the PEP 734 # (PEP 734). We actually drive the private
# public wrapper which only lands in py3.14). # `_interpreters` C module in legacy mode — see
# `tractor.spawn._subint` for why — but py3.13's
# vintage of that private module hangs under our
# multi-trio usage, so we refuse it via the public-
# module presence check.
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.13+ ' f'Spawn method {key!r} requires Python 3.14+.\n'
f'(private stdlib `_interpreters` C module; ' f'(On py3.13 the private `_interpreters` C '
f'the public `concurrent.interpreters` wrapper ' f'module exists but tractor\'s spawn flow '
f'lands in py3.14).\n' f'wedges — see `tractor.spawn._subint` '
f'docstring for details.)\n'
f'Current runtime: {sys.version}' f'Current runtime: {sys.version}'
) )
_ctx = None _ctx = None

View File

@ -25,13 +25,17 @@ IPC-based actor boundary.
Availability Availability
------------ ------------
Runs on py3.13+ via the *private* stdlib `_interpreters` C Requires Python **3.14+**. The private `_interpreters` C
module (which predates the py3.14 public module we actually call into has shipped since 3.13, but
`concurrent.interpreters` stdlib wrapper). See the comment that vintage has a latent bug in its thread/subint
above the `_interpreters` import below for the trade-offs interaction which wedges tractor's spawn flow after
driving the private-API choice. On older runtimes the `_interpreters.create()` the driver `threading.Thread`
module still imports (so the registry stays silently never makes progress inside `_interpreters.exec()`.
introspectable) but `subint_proc()` raises. (Minimal standalone reproductions with threading +
`_interpreters.exec()` work fine on 3.13; only our
multi-trio-task usage triggers the hang. 3.14 fixes it.)
On older runtimes the module still imports (so the registry
stays introspectable) but `subint_proc()` raises.
''' '''
from __future__ import annotations from __future__ import annotations
@ -47,23 +51,31 @@ from trio import TaskStatus
# NOTE: we reach into the *private* `_interpreters` C module # NOTE: we reach into the *private* `_interpreters` C module
# rather than `concurrent.interpreters`' public API because the # for the actual subint create/exec/destroy calls rather than
# public API only exposes PEP 734's `'isolated'` config # `concurrent.interpreters`' public API because the public API
# (per-interp GIL). Under `'isolated'`, any C extension missing # only exposes PEP 734's `'isolated'` config (per-interp GIL).
# the `Py_mod_multiple_interpreters` slot (PEP 684) refuses to # Under `'isolated'`, any C extension missing the
# `Py_mod_multiple_interpreters` slot (PEP 684) refuses to
# import; in our stack that's `msgspec` — which tractor uses # import; in our stack that's `msgspec` — which tractor uses
# pervasively in the IPC layer — so isolated-mode subints can't # pervasively in the IPC layer — so isolated-mode subints
# finish booting the sub-actor's `trio.run()`. msgspec PEP 684 # can't finish booting the sub-actor's `trio.run()`. msgspec
# support is open upstream at jcrist/msgspec#563. # PEP 684 support is open upstream at jcrist/msgspec#563.
# #
# Dropping to the `'legacy'` config keeps the main GIL + lets # Dropping to the `'legacy'` config keeps the main GIL + lets
# existing C extensions load normally while preserving the # existing C extensions load normally while preserving the
# state isolation we actually care about for the actor model # state isolation we actually care about for the actor model
# (separate `sys.modules` / `__main__` / globals). Side win: # (separate `sys.modules` / `__main__` / globals).
# the private `_interpreters` module has shipped since py3.13 #
# (it predates the PEP 734 stdlib landing), so the `subint` # But — we feature-gate on the **public** `concurrent.interpreters`
# backend can run on py3.13+ despite `concurrent.interpreters` # module (3.14+) even though we only call into the private
# itself being 3.14+. # `_interpreters` module. Reason: the private module has
# shipped since 3.13, but the thread/subint interactions
# tractor relies on (`threading.Thread` driving
# `_interpreters.exec(..., legacy)` while a trio loop runs in
# the parent + another inside the subint + IPC between them)
# hang silently on 3.13 and only work cleanly on 3.14. See
# docstring above for the empirical details. Using the public
# module's existence as the gate keeps this check honest.
# #
# Migration path: when msgspec (jcrist/msgspec#563) and any # Migration path: when msgspec (jcrist/msgspec#563) and any
# other PEP 684-holdout C deps opt-in, we can switch to the # other PEP 684-holdout C deps opt-in, we can switch to the
@ -85,6 +97,11 @@ from trio import TaskStatus
# - msgspec PEP 684 upstream tracker: # - msgspec PEP 684 upstream tracker:
# https://github.com/jcrist/msgspec/issues/563 # https://github.com/jcrist/msgspec/issues/563
try: try:
# gate: presence of the public 3.14 stdlib wrapper (we
# don't actually use it below, see NOTE above).
from concurrent import interpreters as _public_interpreters # noqa: F401 # type: ignore
# actual driver: the private C module (also present on
# 3.13 but we refuse that version — see gate above).
import _interpreters # type: ignore import _interpreters # type: ignore
_has_subints: bool = True _has_subints: bool = True
except ImportError: except ImportError:
@ -163,9 +180,10 @@ 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.13+ ' f'The {"subint"!r} spawn backend requires Python 3.14+.\n'
f'(private stdlib `_interpreters` C module; the public ' f'(On py3.13 the private `_interpreters` C module '
f'`concurrent.interpreters` wrapper lands in py3.14).\n' f'exists but tractor\'s spawn flow wedges — see '
f'`tractor.spawn._subint` docstring for details.)\n'
f'Current runtime: {sys.version}' f'Current runtime: {sys.version}'
) )