diff --git a/pyproject.toml b/pyproject.toml index dd7df3b0..ebf044f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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)` diff --git a/tractor/spawn/_spawn.py b/tractor/spawn/_spawn.py index b99fb40c..ec4cc3e4 100644 --- a/tractor/spawn/_spawn.py +++ b/tractor/spawn/_spawn.py @@ -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 diff --git a/tractor/spawn/_subint.py b/tractor/spawn/_subint.py index e740bbb9..a521ad21 100644 --- a/tractor/spawn/_subint.py +++ b/tractor/spawn/_subint.py @@ -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}' )