tractor/tractor/spawn/_subint_forkserver.py

311 lines
11 KiB
Python
Raw Normal View History

Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
# 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/>.
'''
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
Variant-2 (future) "subint forkserver" placeholder reserved
for the eventual subint-isolated-child runtime variant.
> **Status:** placeholder. Today
> `--spawn-backend=subint_forkserver` aliases to
> `main_thread_forkserver_proc` (variant 1, see
> `tractor.spawn._main_thread_forkserver`). A follow-up commit
> in this PR series flips the alias to a `NotImplementedError`
> stub reserving the `'subint_forkserver'` key for the literal
> subint-hosted-child variant once
> [jcrist/msgspec#1026](https://github.com/jcrist/msgspec/issues/1026)
> unblocks PEP 684 isolated-mode subints upstream.
Doc `_subint_forkserver` design + fork semantics Major expansion of the module docstring. Code is unchanged; this lands the architectural reasoning that was previously implicit, plus the POSIX/trio fork mechanics the design relies on. New sections: - "Design rationale" — answers two implicit questions: (1) why a forkserver pattern at all (vs. forking directly from a trio task), (2) why in-process (vs. stdlib `mp.forkserver`'s sidecar process). Documents the three costs the in-process design avoids (sidecar lifecycle, per-spawn IPC, cold-start child) and the tradeoffs we accept in exchange (3.14-only, heavier than `to_thread.run_sync`). - "Implementation status" — clarifies what's actually landed today vs. the envisioned arch: parent's `trio.run()` still lives on main interp (subint- hosted root gated on jcrist/msgspec#1026). Names why the "subint" prefix is correct anyway — same PR series as `_subint.py` / `_subint_fork.py`. - "What survives the fork? — POSIX semantics" — POSIX preserves only the calling thread, so the `trio.run()` thread is gone in the child. Includes a small parent/child thread-survival table and covers the four artifact classes that DO cross the fork boundary (inherited fds, COW memory, Python thread state, user-level locks) and how each is handled. - "FYI: how this dodges the `trio.run()` × `fork()` hazards" — itemizes each class of trio process- global state (wakeup-fd, `epoll`/`kqueue`, threadpool, cancel scopes / nurseries, `atexit`, foreign-language I/O) and explains how the forkserver-thread design avoids each. Also, - bump the gated msgspec issue link from `jcrist/msgspec#563` to `jcrist/msgspec#1026` (the PEP 684 isolated-mode tracker). (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 3ab99d557ab7307af9b967ada8f744da6ded5b59)
2026-04-27 22:16:50 +00:00
Future arch what subints would buy us
---------------------------------------
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
When msgspec#1026 unblocks isolated-mode subints (PEP 684
per-interp GIL), three concrete wins land these are the
reason the `'subint_forkserver'` key is reserved as a
distinct backend rather than just folded into
`'main_thread_forkserver'`:
**(1) Cheaper forks (smaller main-interp COW image)**
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
Today (variant 1) the parent's main interp carries the full
tractor stack: trio runtime, msgspec codecs, IPC layer,
every user module the actor imported. When the forkserver
worker calls `os.fork()` the child inherits ALL of that as
COW memory even though most gets overwritten when the
child boots its own `trio.run()`.
Variant 2 moves the parent's `trio.run()` into a subint (its
own `sys.modules` / `__main__` / globals). The main interp
**stays minimal** just the forkserver-thread plumbing +
bare CPython. The main interp becomes the *literal*
forkserver: an intentionally-empty execution context whose
only job is to call `os.fork()` cleanly. Inherited COW image
shrinks proportionally.
**(2) True parallelism between forkserver and trio
(per-interp GIL)**
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
Variant-1 today: the forkserver worker and the trio.run()
thread share the main GIL when one runs the other waits.
Spawn requests briefly stall trio while the worker takes
the GIL to call `os.fork()`. PEP 684 isolated-mode gives
each subint its own GIL: forkserver thread on main + trio
on subint actually run in parallel. Spawn latency drops,
trio loop doesn't notice the fork happening.
**(3) Multi-actor-per-process (the architectural prize)**
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
The bigger payoff and the reason `_subint.py` (the in-thread
`subint` backend) exists in parallel with this module. With
per-interp-GIL subints, one process can host:
- main interp: forkserver thread + bookkeeping
- subint A: actor 1's `trio.run()`
- subint B: actor 2's `trio.run()`
- subint C: ...
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
`os.fork()` becomes the **last-resort** spawn used only
when a new OS process is actually required (cgroups,
namespaces, security boundary, multi-host distribution).
Within a single process, subint-per-actor is radically
cheaper: no fork, no COW, no inherited-fd cleanup just
`_interpreters.create()` + `_interpreters.exec()`.
The three backends converge on a coherent story:
- `subint` in-process spawn (cheap, GIL-isolated),
- `main_thread_forkserver` cross-process spawn today
(variant 1, working),
- `subint_forkserver` cross-process spawn with
isolated-subint child (variant 2, this module, future).
What lives here today
---------------------
- `run_subint_in_worker_thread()` companion primitive to
`_main_thread_forkserver.fork_from_worker_thread()`. Creates
a fresh `legacy`-config sub-interpreter and drives a given
bootstrap code string through `_interpreters.exec()` on a
dedicated worker thread; destroys the subint after the
thread joins. Used today by the
`subint_fork_from_main_thread_smoketest.py` feasibility
check; will be wired into the variant-2
`subint_forkserver_proc` spawn-coroutine when it lands.
- (legacy re-exports of fork primitives kept for backward-
compatible imports until external consumers migrate to
`_main_thread_forkserver`)
What will live here when variant 2 ships
----------------------------------------
- `subint_forkserver_proc()` the variant-2 spawn-backend
coroutine. Same fork machinery as variant 1, but the
fork-child enters a fresh subint (via
`run_subint_in_worker_thread`) before booting its
`trio.run()`. Net effect: child runtime is GIL-isolated
from the parent + any sibling actors in the same process.
- A stub `subint_forkserver_proc` is added in a follow-up
commit that raises `NotImplementedError(...)` pointing at
this docstring + jcrist/msgspec#1026 + tractor #379, so
`--spawn-backend=subint_forkserver` errors cleanly today
rather than silently aliasing variant 1.
Add trio-parent tests for `_subint_forkserver` New pytest module `tests/spawn/test_subint_forkserver.py` drives the forkserver primitives from inside a real `trio.run()` in the parent — the runtime shape tractor will actually use when we wire up a `subint_forkserver` spawn backend proper. Complements the standalone no-trio-in-parent `ai/conc-anal/subint_fork_from_main_thread_smoketest.py`. Deats, - new test pkg `tests/spawn/` (+ empty `__init__.py`) - two tests, both `@pytest.mark.timeout(30, method='thread')` for the GIL-hostage safety reason doc'd in `ai/conc-anal/subint_sigint_starvation_issue.md`: - `test_fork_from_worker_thread_via_trio` — parent-side plumbing baseline. `trio.run()` off-loads forkserver prims via `trio.to_thread.run_sync()` + asserts the child reaps cleanly - `test_fork_and_run_trio_in_child` — end-to-end: forked child calls `run_subint_in_worker_thread()` with a bootstrap str that does `trio.run()` in a fresh subint - both tests wrap the inner `trio.run()` in a `dump_on_hang()` for post-mortem if the outer `pytest-timeout` fires - intentionally NOT using `--spawn-backend` — the tests drive the primitives directly rather than going through tractor's spawn-method registry (which the forkserver isn't plugged into yet) Also, rename `run_trio_in_subint()` → `run_subint_in_worker_thread()` for naming consistency with the sibling `fork_from_worker_thread()`. The action is really "host a subint on a worker thread", not specifically "run trio" — trio just happens to be the typical payload. Propagate the rename to the smoketest. Further, add a "TODO — cleanup gated on msgspec PEP 684 support" section to the `_subint_forkserver` module docstring: flags the dedicated-`threading.Thread` design as potentially-revisable once isolated-mode subints are viable in tractor. Cross-refs `msgspec#563` + `tractor#379` and points at an audit-plan conc-anal doc we'll add next. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 25e400d52683bd1612e4426e28c71ed232aadb9b)
2026-04-22 22:00:06 +00:00
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
See also
--------
Split forkserver backend into variant 1/2 mods The `subint_forkserver` name was always aspirational — today's impl forks from a regular main-interp worker thread and the child runs trio on its own main interp; NO subinterp anywhere in parent or child. Splitting the backend into two clearly-named variants drops the lie: - **variant 1** — `main_thread_forkserver` (the working impl). New `SpawnMethodKey` literal + `_methods` dispatch entry + `_runtime.Actor._from_parent()` match-arm. The spawn-coro `subint_forkserver_proc` moves to `_main_thread_forkserver` and is renamed `main_thread_forkserver_proc()`. - **variant 2** — `subint_forkserver` (future, reserved). Module shrinks to a placeholder describing the variant-2 design (subint-isolated child runtime, gated on jcrist/msgspec#1026 + PEP 684). Today the legacy `'subint_forkserver'` key aliases to `main_thread_forkserver_proc` so existing `--spawn-backend=subint_forkserver` invocations keep working; flipped to a `NotImplementedError` stub in a follow-up. Deats, - `Actor._from_parent()` spawn-method gate now accepts both `'main_thread_forkserver'` and `'subint_forkserver'` (both go through the IPC-`SpawnSpec` path). - the variant-1 spawn-coro stamps its own `SpawnSpec` / log lines with `spawn_method='main_thread_forkserver'` so subactor renders reflect the actual mechanism. - docstring reorg: trio×fork hazard breakdown, POSIX fork-survival semantics, in-process-vs-stdlib forkserver design notes, and the TODO/cleanup section all move from `_subint_forkserver` to `_main_thread_forkserver` (lives with the working code). `_subint_forkserver` keeps a tight forward- looking doc that motivates the reserved key. - `run_subint_in_worker_thread()` stays in `_subint_forkserver` as the companion primitive — it's the subint counterpart to `fork_from_worker_thread()` and will plug into the future variant-2 spawn-coro. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 57dae0e4a62a8edd21753e7ec207697c9994d159)
2026-04-27 23:28:11 +00:00
- `tractor.spawn._main_thread_forkserver` variant 1,
working today; for the full design rationale, fork-
semantics analysis, and trio×fork hazard breakdown.
- `tractor.spawn._subint` the in-thread `subint` backend
(one process, one actor per subint, no fork).
- `tractor.spawn._subint_fork` RFC stub for the
fork-from-non-main-subint strategy that is blocked at the
CPython level.
- [#379](https://github.com/goodboy/tractor/issues/379)
subint backend umbrella tracking issue.
- [jcrist/msgspec#1026](https://github.com/jcrist/msgspec/issues/1026)
upstream blocker for PEP 684 isolated-mode subints.
- [#450](https://github.com/goodboy/tractor/issues/450) —
thread-constraints audit follow-up tied to msgspec#1026.
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
'''
from __future__ import annotations
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
import sys
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
import threading
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
from functools import partial
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
from typing import (
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
Any,
Scaffold `child_sigint` modes for forkserver Add configuration surface for future child-side SIGINT plumbing in `subint_forkserver_proc` without wiring up the actual trio-native SIGINT bridge — lifting one entry-guard clause will flip the `'trio'` branch live once the underlying fork-prelude plumbing is implemented. Deats, - new `ChildSigintMode = Literal['ipc', 'trio']` type + `_DEFAULT_CHILD_SIGINT = 'ipc'` module-level default. Docstring block enumerates both: - `'ipc'` (default, currently the only implemented mode): no child-side SIGINT handler — `trio.run()` is on the fork-inherited non-main thread where `signal.set_wakeup_fd()` is main-thread-only, so cancellation flows exclusively via the parent's `Portal.cancel_actor()` IPC path. Known gap: orphan children don't respond to SIGINT (`test_orphaned_subactor_sigint_cleanup_DRAFT`) - `'trio'` (scaffolded only): manual SIGINT → trio-cancel bridge in the fork-child prelude so external Ctrl-C reaches stuck grandchildren even w/ a dead parent - `subint_forkserver_proc` pulls `child_sigint` out of `proc_kwargs` (matches how `trio_proc` threads config to `open_process`, keeps `start_actor(proc_kwargs=...)` as the ergonomic entry point); validates membership + raises `NotImplementedError` for `'trio'` at the backend-entry guard - `_child_target` grows a `match child_sigint:` arm that slots in the future `'trio'` impl without restructuring — today only the `'ipc'` case is reachable - module docstring "Still-open work" list grows a bullet pointing at this config + the xfail'd orphan-SIGINT test No behavioral change on the default path — `'ipc'` is the existing flow. Scaffolding only. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit dcd5c1ff400a5eddf82ef50bb1dacff5342e6020)
2026-04-23 00:08:30 +00:00
Literal,
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
TYPE_CHECKING,
)
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
import trio
from trio import TaskStatus
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
from tractor.log import get_logger
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
from tractor.msg import (
types as msgtypes,
pretty_struct,
)
from tractor.runtime._state import current_actor
from tractor.runtime._portal import Portal
from ._spawn import (
cancel_on_completion,
soft_kill,
)
# Lower-level fork primitives — see module docstring for the
# split rationale. `_subint_forkserver` builds tractor's
# subint-family spawn backend on top of these.
from ._main_thread_forkserver import (
_close_inherited_fds as _close_inherited_fds,
_format_child_exit as _format_child_exit,
fork_from_worker_thread as fork_from_worker_thread,
wait_child as wait_child,
_ForkedProc,
)
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
if TYPE_CHECKING:
Wire `subint_forkserver` as first-class backend Promote `_subint_forkserver` from primitives-only into a registered spawn backend: `'subint_forkserver'` is now a `SpawnMethodKey` literal, dispatched via `_methods` to the new `subint_forkserver_proc()` target, feature-gated under the existing `subint`-family py3.14+ case, and selectable via `--spawn-backend=subint_forkserver`. Deats, - new `subint_forkserver_proc()` spawn target in `_subint_forkserver`: - mirrors `trio_proc()`'s supervision model — real OS subprocess so `Portal.cancel_actor()` + `soft_kill()` on graceful teardown, `os.kill(SIGKILL)` on hard-reap (no `_interpreters.destroy()` race to fuss over bc the child lives in its own process) - only real diff from `trio_proc` is the spawn mechanism: fork from a main-interp worker thread via `fork_from_worker_thread()` (off-loaded to trio's thread pool) instead of `trio.lowlevel.open_process()` - child-side `_child_target` closure runs `tractor._child._actor_child_main()` with `spawn_method='trio'` — the child is a regular trio actor, "subint_forkserver" names how the parent spawned, not what the child runs - new `_ForkedProc` class — thin `trio.Process`-compatible shim around a raw OS pid: `.poll()` via `waitpid(WNOHANG)`, async `.wait()` off-loaded to a trio cache thread, `.kill()` via `SIGKILL`, `.returncode` cached for repeat calls. `.stdin`/`.stdout`/`.stderr` are `None` (fork-w/o-exec inherits parent FDs; we don't marshal them) which matches `soft_kill()`'s `is not None` guards Also, new backend-tier test `test_subint_forkserver_spawn_basic` drives the registered backend end-to-end via `open_root_actor` + `open_nursery` + `run_in_actor` w/ a trivial portal-RPC round-trip. Uses a `forkserver_spawn_method` fixture to flip `_spawn_method`/`_ctx` for the test's duration + restore on teardown (so other session-level tests don't observe the global flip). Test module docstring reworked to describe the three tiers now covered: (1) primitive-level, (2) parent-trio-driven primitives, (3) full registered backend. Status: still-open work (tracked on `tractor#379`) doc'd inline in the module docstring — no cancel/hard-kill stress coverage yet, child-side subint-hosted root runtime still future (gated on `msgspec#563`), thread-hygiene audit pending the same unblock. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 26914fde753d357920a6366c7e8ec15fcdbc0323)
2026-04-22 22:49:23 +00:00
from tractor.discovery._addr import UnwrappedAddress
from tractor.ipc import (
_server,
)
from tractor.runtime._runtime import Actor
from tractor.runtime._supervise import ActorNursery
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
log = get_logger('tractor')
Scaffold `child_sigint` modes for forkserver Add configuration surface for future child-side SIGINT plumbing in `subint_forkserver_proc` without wiring up the actual trio-native SIGINT bridge — lifting one entry-guard clause will flip the `'trio'` branch live once the underlying fork-prelude plumbing is implemented. Deats, - new `ChildSigintMode = Literal['ipc', 'trio']` type + `_DEFAULT_CHILD_SIGINT = 'ipc'` module-level default. Docstring block enumerates both: - `'ipc'` (default, currently the only implemented mode): no child-side SIGINT handler — `trio.run()` is on the fork-inherited non-main thread where `signal.set_wakeup_fd()` is main-thread-only, so cancellation flows exclusively via the parent's `Portal.cancel_actor()` IPC path. Known gap: orphan children don't respond to SIGINT (`test_orphaned_subactor_sigint_cleanup_DRAFT`) - `'trio'` (scaffolded only): manual SIGINT → trio-cancel bridge in the fork-child prelude so external Ctrl-C reaches stuck grandchildren even w/ a dead parent - `subint_forkserver_proc` pulls `child_sigint` out of `proc_kwargs` (matches how `trio_proc` threads config to `open_process`, keeps `start_actor(proc_kwargs=...)` as the ergonomic entry point); validates membership + raises `NotImplementedError` for `'trio'` at the backend-entry guard - `_child_target` grows a `match child_sigint:` arm that slots in the future `'trio'` impl without restructuring — today only the `'ipc'` case is reachable - module docstring "Still-open work" list grows a bullet pointing at this config + the xfail'd orphan-SIGINT test No behavioral change on the default path — `'ipc'` is the existing flow. Scaffolding only. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit dcd5c1ff400a5eddf82ef50bb1dacff5342e6020)
2026-04-23 00:08:30 +00:00
# Configurable child-side SIGINT handling for forkserver-spawned
# subactors. Threaded through `subint_forkserver_proc`'s
# `proc_kwargs` under the `'child_sigint'` key.
#
# - `'ipc'` (default, currently the only implemented mode):
# child has NO trio-level SIGINT handler — trio.run() is on
# the fork-inherited non-main thread, `signal.set_wakeup_fd()`
# is main-thread-only. Cancellation flows exclusively via
# the parent's `Portal.cancel_actor()` IPC path. Safe +
# deterministic for nursery-structured apps where the parent
# is always the cancel authority. Known gap: orphan
# (post-parent-SIGKILL) children don't respond to SIGINT
# — see `test_orphaned_subactor_sigint_cleanup_DRAFT`.
#
# - `'trio'` (**not yet implemented**): install a manual
# SIGINT → trio-cancel bridge in the child's fork prelude
# (pre-`trio.run()`) so external Ctrl-C reaches stuck
# grandchildren even with a dead parent. Adds signal-
# handling surface the `'ipc'` default cleanly avoids; only
# pay for it when externally-interruptible children actually
# matter (e.g. CLI tool grandchildren).
ChildSigintMode = Literal['ipc', 'trio']
_DEFAULT_CHILD_SIGINT: ChildSigintMode = 'ipc'
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
# Feature-gate: py3.14+ via the public `concurrent.interpreters`
# wrapper. Matches the gate in `tractor.spawn._subint` —
# see that module's docstring for why we require the public
# API's presence even though we reach into the private
# `_interpreters` C module for actual calls.
try:
from concurrent import interpreters as _public_interpreters # noqa: F401 # type: ignore
import _interpreters # type: ignore
_has_subints: bool = True
except ImportError:
_interpreters = None # type: ignore
_has_subints: bool = False
Add trio-parent tests for `_subint_forkserver` New pytest module `tests/spawn/test_subint_forkserver.py` drives the forkserver primitives from inside a real `trio.run()` in the parent — the runtime shape tractor will actually use when we wire up a `subint_forkserver` spawn backend proper. Complements the standalone no-trio-in-parent `ai/conc-anal/subint_fork_from_main_thread_smoketest.py`. Deats, - new test pkg `tests/spawn/` (+ empty `__init__.py`) - two tests, both `@pytest.mark.timeout(30, method='thread')` for the GIL-hostage safety reason doc'd in `ai/conc-anal/subint_sigint_starvation_issue.md`: - `test_fork_from_worker_thread_via_trio` — parent-side plumbing baseline. `trio.run()` off-loads forkserver prims via `trio.to_thread.run_sync()` + asserts the child reaps cleanly - `test_fork_and_run_trio_in_child` — end-to-end: forked child calls `run_subint_in_worker_thread()` with a bootstrap str that does `trio.run()` in a fresh subint - both tests wrap the inner `trio.run()` in a `dump_on_hang()` for post-mortem if the outer `pytest-timeout` fires - intentionally NOT using `--spawn-backend` — the tests drive the primitives directly rather than going through tractor's spawn-method registry (which the forkserver isn't plugged into yet) Also, rename `run_trio_in_subint()` → `run_subint_in_worker_thread()` for naming consistency with the sibling `fork_from_worker_thread()`. The action is really "host a subint on a worker thread", not specifically "run trio" — trio just happens to be the typical payload. Propagate the rename to the smoketest. Further, add a "TODO — cleanup gated on msgspec PEP 684 support" section to the `_subint_forkserver` module docstring: flags the dedicated-`threading.Thread` design as potentially-revisable once isolated-mode subints are viable in tractor. Cross-refs `msgspec#563` + `tractor#379` and points at an audit-plan conc-anal doc we'll add next. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 25e400d52683bd1612e4426e28c71ed232aadb9b)
2026-04-22 22:00:06 +00:00
def run_subint_in_worker_thread(
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
bootstrap: str,
*,
thread_name: str = 'subint-trio',
join_timeout: float = 10.0,
) -> None:
'''
Add trio-parent tests for `_subint_forkserver` New pytest module `tests/spawn/test_subint_forkserver.py` drives the forkserver primitives from inside a real `trio.run()` in the parent — the runtime shape tractor will actually use when we wire up a `subint_forkserver` spawn backend proper. Complements the standalone no-trio-in-parent `ai/conc-anal/subint_fork_from_main_thread_smoketest.py`. Deats, - new test pkg `tests/spawn/` (+ empty `__init__.py`) - two tests, both `@pytest.mark.timeout(30, method='thread')` for the GIL-hostage safety reason doc'd in `ai/conc-anal/subint_sigint_starvation_issue.md`: - `test_fork_from_worker_thread_via_trio` — parent-side plumbing baseline. `trio.run()` off-loads forkserver prims via `trio.to_thread.run_sync()` + asserts the child reaps cleanly - `test_fork_and_run_trio_in_child` — end-to-end: forked child calls `run_subint_in_worker_thread()` with a bootstrap str that does `trio.run()` in a fresh subint - both tests wrap the inner `trio.run()` in a `dump_on_hang()` for post-mortem if the outer `pytest-timeout` fires - intentionally NOT using `--spawn-backend` — the tests drive the primitives directly rather than going through tractor's spawn-method registry (which the forkserver isn't plugged into yet) Also, rename `run_trio_in_subint()` → `run_subint_in_worker_thread()` for naming consistency with the sibling `fork_from_worker_thread()`. The action is really "host a subint on a worker thread", not specifically "run trio" — trio just happens to be the typical payload. Propagate the rename to the smoketest. Further, add a "TODO — cleanup gated on msgspec PEP 684 support" section to the `_subint_forkserver` module docstring: flags the dedicated-`threading.Thread` design as potentially-revisable once isolated-mode subints are viable in tractor. Cross-refs `msgspec#563` + `tractor#379` and points at an audit-plan conc-anal doc we'll add next. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 25e400d52683bd1612e4426e28c71ed232aadb9b)
2026-04-22 22:00:06 +00:00
Create a fresh legacy-config sub-interpreter and drive
the given `bootstrap` code string through
`_interpreters.exec()` on a dedicated worker thread.
Naming mirrors `fork_from_worker_thread()`:
"<action>_in_worker_thread" the action here is "run a
subint", not "run trio" per se. Typical `bootstrap`
content does import `trio` + call `trio.run()`, but
nothing about this primitive requires trio; it's a
generic "host a subint on a worker thread" helper.
Intended mainly for use inside a fork-child (see
`tractor.spawn._subint_forkserver` module docstring) but
works anywhere.
See `tractor.spawn._subint.subint_proc` for the matching
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
pattern tractor uses at the sub-actor level.
Destroys the subint after the thread joins.
'''
if not _has_subints:
raise RuntimeError(
'subint-forkserver primitives require Python '
'3.14+.'
)
interp_id: int = _interpreters.create('legacy')
log.runtime(
f'Created child-side subint for trio.run()\n'
f'(>\n'
f' |_interp_id={interp_id}\n'
)
err: BaseException | None = None
def _drive() -> None:
nonlocal err
try:
_interpreters.exec(interp_id, bootstrap)
except BaseException as e:
err = e
log.exception(
f'Failed to .exec() in subint ??\n'
f'_interpreters.exec(\n'
f' interp_id={interp_id!r},\n'
f' bootstrap={bootstrap!r},\n'
f') => {err!r}\n'
)
Lift fork prims into `_subint_forkserver` mod The smoketest (prior commit) empirically validated the "fork-from-main-interp-worker-thread" arch on py3.14. Promote the validated primitives out of the `ai/conc-anal/` smoketest into `tractor.spawn._subint_forkserver` so they can eventually be wired into a real "subint forkserver" spawn backend. Deats, - new module `tractor/spawn/_subint_forkserver.py` (337 LOC): - `fork_from_worker_thread(child_target, thread_name)` — spawn a main-interp `threading.Thread`, call `os.fork()` from it, shuttle the child pid back to main via a pipe - `run_trio_in_subint(bootstrap, ...)` — post-fork helper: create a fresh subint + drive `_interpreters.exec()` on a dedicated worker thread running the `bootstrap` str (typically imports `trio`, defines an async entry, calls `trio.run()`) - `wait_child(pid, expect_exit_ok)` — `os.waitpid()` + pass/fail classification reusable from harness AND the eventual real spawn path - feature-gated py3.14+ via the public `concurrent.interpreters` presence check; matches the gate in `tractor.spawn._subint` - module docstring doc's the CPython-block context (cross-refs `_subint_fork` stub + the two `conc-anal/` docs) and status: EXPERIMENTAL, not yet registered in `_spawn._methods` Also, refactor the smoketest `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` to import the primitives from the new module rather than inline its own copies. Keeps the smoketest and the tractor-side impl in sync as the forkserver design evolves; the smoketest remains a zero-`tractor`-runtime CPython-level check (imports ONLY the three primitives, no runtime bring-up). Status: next step is to drive these from a parent-side `trio.run()` and hook the returned child pid into the normal actor-nursery/IPC flow — then register `subint_forkserver` as a `SpawnMethodKey` in `_spawn.py`. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code (cherry picked from commit 82332fbceb79b3b872eee4ea653dd2375d76ad00)
2026-04-22 21:03:15 +00:00
worker: threading.Thread = threading.Thread(
target=_drive,
name=thread_name,
daemon=False,
)
worker.start()
worker.join(timeout=join_timeout)
try:
_interpreters.destroy(interp_id)
except _interpreters.InterpreterError as e:
log.warning(
f'Could not destroy child-side subint '
f'{interp_id}: {e}'
)
if worker.is_alive():
raise RuntimeError(
f'child-side subint trio-driver thread '
f'{thread_name!r} did not return within '
f'{join_timeout}s.'
)
if err is not None:
raise err