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/>.
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
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
|
|
|
|
|
2026-04-27 22:20:10 +00:00
|
|
|
|
Future arch — what subints would buy us
|
|
|
|
|
|
---------------------------------------
|
|
|
|
|
|
|
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'`:
|
2026-04-27 22:20:10 +00:00
|
|
|
|
|
|
|
|
|
|
**(1) Cheaper forks (smaller main-interp COW image)**
|
|
|
|
|
|
|
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.
|
2026-04-27 22:20:10 +00:00
|
|
|
|
|
|
|
|
|
|
**(2) True parallelism between forkserver and trio
|
|
|
|
|
|
(per-interp GIL)**
|
|
|
|
|
|
|
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.
|
2026-04-27 22:20:10 +00:00
|
|
|
|
|
|
|
|
|
|
**(3) Multi-actor-per-process (the architectural prize)**
|
|
|
|
|
|
|
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:
|
2026-04-27 22:20:10 +00:00
|
|
|
|
|
|
|
|
|
|
- main interp: forkserver thread + bookkeeping
|
|
|
|
|
|
- subint A: actor 1's `trio.run()`
|
|
|
|
|
|
- subint B: actor 2's `trio.run()`
|
|
|
|
|
|
- subint C: ...
|
|
|
|
|
|
|
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.
|
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
|
|
|
|
|
|
--------
|
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,
|
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,
|
|
|
|
|
|
)
|
2026-04-27 23:04:26 +00:00
|
|
|
|
# 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')
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
'''
|
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
|
2026-04-23 22:17:56 +00:00
|
|
|
|
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
|