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
subint_forkserver_backend
Gud Boi 2026-04-20 14:01:49 -04:00
parent 09466a1e9d
commit 34d9d482e4
3 changed files with 77 additions and 31 deletions

View File

@ -44,11 +44,12 @@ dependencies = [
"tricycle>=0.4.1,<0.5",
"wrapt>=1.16.0,<2",
"colorlog>=6.8.2,<7",
# built-in multi-actor `pdb` REPL
"pdbp>=1.8.2,<2", # windows only (from `pdbp`)
# typed IPC msging
"msgspec>=0.20.0",
"cffi>=1.17.1",
"bidict>=0.23.1",
"multiaddr>=0.2.0",
"platformdirs>=4.4.0",
@ -64,10 +65,13 @@ dev = [
]
devx = [
# `tractor.devx` tooling
"greenback>=1.2.1,<2", # TODO? 3.14 greenlet on nix?
"stackscope>=0.2.2,<0.3",
# ^ requires this?
"typing-extensions>=4.14.1",
# {include-group = 'sync_pause'}, # XXX, no 3.14 yet!
]
sync_pause = [
"greenback>=1.2.1,<2", # TODO? 3.14 greenlet on nix?
]
testing = [
# test suite
@ -85,6 +89,14 @@ repl = [
lint = [
"ruff>=0.9.6"
]
# XXX, used for linux-only hi perf eventfd+shm channels
# now mostly moved over to `hotbaud`.
eventfd = [
"cffi>=1.17.1",
]
subints = [
"msgspec>=0.21.0",
]
# TODO, add these with sane versions; were originally in
# `requirements-docs.txt`..
# docs = [
@ -93,6 +105,17 @@ lint = [
# ]
# ------ dependency-groups ------
[tool.uv.dependency-groups]
# for subints, we require 3.14+ due to 2 issues,
# - hanging behaviour for various multi-task teardown cases (see
# "Availability" section in the `tractor.spawn._subints` doc string).
# - `msgspec` support which is oustanding per PEP 684 upstream tracker:
# https://github.com/jcrist/msgspec/issues/563
#
# https://docs.astral.sh/uv/concepts/projects/dependencies/#group-requires-python
subints = {requires-python = ">=3.14"}
eventfd = {requires-python = ">=3.13, <3.14"}
sync_pause = {requires-python = ">=3.13, <3.14"}
[tool.uv.sources]
# XXX NOTE, only for @goodboy's hacking on `pprint(sort_dicts=False)`

View File

@ -117,16 +117,21 @@ def try_set_start_method(
case 'subint':
# 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).
# py3.14 public `concurrent.interpreters` wrapper
# (PEP 734). We actually drive the private
# `_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
if not _has_subints:
raise RuntimeError(
f'Spawn method {key!r} requires Python 3.13+ '
f'(private stdlib `_interpreters` C module; '
f'the public `concurrent.interpreters` wrapper '
f'lands in py3.14).\n'
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'
f'Current runtime: {sys.version}'
)
_ctx = None

View File

@ -25,13 +25,17 @@ IPC-based actor boundary.
Availability
------------
Runs on py3.13+ via the *private* stdlib `_interpreters` C
module (which predates the py3.14 public
`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.
Requires Python **3.14+**. The private `_interpreters` C
module we actually call into has shipped since 3.13, but
that vintage has a latent bug in its thread/subint
interaction which wedges tractor's spawn flow after
`_interpreters.create()` the driver `threading.Thread`
silently never makes progress inside `_interpreters.exec()`.
(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
@ -47,23 +51,31 @@ from trio import TaskStatus
# NOTE: we reach into the *private* `_interpreters` C module
# rather than `concurrent.interpreters`' public API because the
# public API only exposes PEP 734's `'isolated'` config
# (per-interp GIL). Under `'isolated'`, any C extension missing
# the `Py_mod_multiple_interpreters` slot (PEP 684) refuses to
# for the actual subint create/exec/destroy calls rather than
# `concurrent.interpreters`' public API because the public API
# only exposes PEP 734's `'isolated'` config (per-interp GIL).
# 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
# pervasively in the IPC layer — so isolated-mode subints can't
# finish booting the sub-actor's `trio.run()`. msgspec PEP 684
# support is open upstream at jcrist/msgspec#563.
# pervasively in the IPC layer — so isolated-mode subints
# can't finish booting the sub-actor's `trio.run()`. msgspec
# PEP 684 support is open upstream at jcrist/msgspec#563.
#
# Dropping to the `'legacy'` config keeps the main GIL + lets
# existing C extensions load normally while preserving the
# 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+.
# (separate `sys.modules` / `__main__` / globals).
#
# But — we feature-gate on the **public** `concurrent.interpreters`
# module (3.14+) even though we only call into the private
# `_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
# 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:
# https://github.com/jcrist/msgspec/issues/563
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
_has_subints: bool = True
except ImportError:
@ -163,9 +180,10 @@ async def subint_proc(
'''
if not _has_subints:
raise RuntimeError(
f'The {"subint"!r} spawn backend requires Python 3.13+ '
f'(private stdlib `_interpreters` C module; the public '
f'`concurrent.interpreters` wrapper lands in py3.14).\n'
f'The {"subint"!r} spawn backend requires Python 3.14+.\n'
f'(On py3.13 the private `_interpreters` C module '
f'exists but tractor\'s spawn flow wedges — see '
f'`tractor.spawn._subint` docstring for details.)\n'
f'Current runtime: {sys.version}'
)