2021-12-13 18:08:32 +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-06-10 00:16:02 +00:00
|
|
|
The "bootloader" for sub-actors spawned via the native `trio`
|
|
|
|
|
backend (the default `python -m tractor._child` CLI entry) and
|
|
|
|
|
the in-process `subint` backend (`tractor.spawn._subint`).
|
2021-12-13 18:08:32 +00:00
|
|
|
|
2020-07-29 18:39:38 +00:00
|
|
|
"""
|
2026-06-10 00:16:02 +00:00
|
|
|
from __future__ import annotations
|
2020-07-27 17:55:37 +00:00
|
|
|
import argparse
|
2020-07-28 00:05:00 +00:00
|
|
|
from ast import literal_eval
|
2026-06-10 00:16:02 +00:00
|
|
|
from typing import TYPE_CHECKING
|
2020-07-28 00:05:00 +00:00
|
|
|
|
Mv core mods to `runtime/`, `spawn/`, `discovery/` subpkgs
Restructure the flat `tractor/` top-level private mods
into (more nested) subpackages:
- `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`,
`_supervise`
- `spawn/`: `_spawn`, `_entry`, `_forkserver_override`,
`_mp_fixup_main`
- `discovery/`: `_addr`, `_discovery`, `_multiaddr`
Each subpkg `__init__.py` is kept lazy (no eager
imports) to avoid circular import issues.
Also,
- update all intra-pkg imports across ~35 mods to use
the new subpkg paths (e.g. `from .runtime._state`
instead of `from ._state`)
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-23 22:42:16 +00:00
|
|
|
from .runtime._runtime import Actor
|
|
|
|
|
from .spawn._entry import _trio_main
|
2020-07-27 17:55:37 +00:00
|
|
|
|
2026-06-10 00:16:02 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from .discovery._addr import UnwrappedAddress
|
|
|
|
|
from .spawn._spawn import SpawnMethodKey
|
|
|
|
|
|
2020-07-27 17:55:37 +00:00
|
|
|
|
2020-07-28 00:05:00 +00:00
|
|
|
def parse_uid(arg):
|
2020-07-28 14:55:11 +00:00
|
|
|
name, uuid = literal_eval(arg) # ensure 2 elements
|
|
|
|
|
return str(name), str(uuid) # ensures str encoding
|
2020-07-28 00:05:00 +00:00
|
|
|
|
|
|
|
|
def parse_ipaddr(arg):
|
2025-03-23 05:18:01 +00:00
|
|
|
try:
|
|
|
|
|
return literal_eval(arg)
|
|
|
|
|
|
|
|
|
|
except (ValueError, SyntaxError):
|
|
|
|
|
# UDS: try to interpret as a straight up str
|
|
|
|
|
return arg
|
2020-07-28 00:05:00 +00:00
|
|
|
|
2020-07-20 19:18:38 +00:00
|
|
|
|
2026-06-10 00:16:02 +00:00
|
|
|
def _actor_child_main(
|
|
|
|
|
uid: tuple[str, str],
|
|
|
|
|
loglevel: str | None,
|
|
|
|
|
parent_addr: UnwrappedAddress | None,
|
|
|
|
|
infect_asyncio: bool,
|
|
|
|
|
spawn_method: SpawnMethodKey = 'trio',
|
|
|
|
|
|
|
|
|
|
) -> None:
|
|
|
|
|
'''
|
|
|
|
|
Construct the child `Actor` and dispatch to `_trio_main()`.
|
|
|
|
|
|
|
|
|
|
Shared entry shape used by both the `python -m tractor._child`
|
|
|
|
|
CLI (trio/mp subproc backends) and the `subint` backend, which
|
|
|
|
|
invokes this from inside a fresh `concurrent.interpreters`
|
|
|
|
|
sub-interpreter via `Interpreter.call()`.
|
|
|
|
|
|
|
|
|
|
'''
|
Add `tractor.trionics.patches` subpkg + first fix
With a seminal patch fixing `trio`'s `WakeupSocketpair.drain()` which
can busy-loop due to lack of handling `EOF`.
New `tractor.trionics.patches` subpkg housing defensive monkey-patches
for upstream `trio` bugs we've encountered while running `tractor`
— particularly as of recent, fork-survival edge cases that haven't been
filed/fixed upstream yet. Each patch is idempotent, version-gated via
`is_needed()`, and carries a `# REMOVE WHEN:` marker pointing at the
upstream release whose adoption allows deletion.
Subpkg layout + per-patch contract documented in
`tractor/trionics/patches/README.md` — `apply()` / `is_needed()`
/ `repro()` API, registry pattern via `_PATCHES` in `__init__.py`,
single-call entry point `apply_all()`.
First patch, `_wakeup_socketpair`:
- `trio`'s `WakeupSocketpair.drain()` loops on `recv(64KB)` and exits
ONLY on `BlockingIOError`, NEVER on `recv() == b''` (peer-closed FIN).
- under `fork()`-spawning backends the COW-inherited socketpair fds
& `_close_inherited_fds()` teardown can leave a `WakeupSocketpair`
instance whose write-end is closed, and `drain()` then **spins forever
in C with no Python checkpoints**,
- this obviously burns 100% CPU and no signal delivery.
Standalone repro:
from trio._core._wakeup_socketpair import WakeupSocketpair
ws = WakeupSocketpair()
ws.write_sock.close()
ws.drain() # spins forever
Patch is one-line — break the drain loop on b'' EOF.
Manifested as two distinct test failures:
- `tests/test_multi_program.py::test_register_duplicate_name` hung at
100% CPU on the busy-loop directly (fork child's worker thread)
- `tests/test_infected_asyncio.py::test_aio_simple_error` Mode-A
deadlock — busy-loop wedged trio's scheduler inside `start_guest_run`,
both threads parked in `epoll_wait`, no TCP connect-back to parent
ever happened.
Same patch fixes both. Restored 99.7% pass rate on full
suite under `--spawn-backend=main_thread_forkserver`
(was hanging indefinitely before).
Wired into `tractor._child._actor_child_main` via `apply_all()` BEFORE
any trio runtime init. Harmless on non-fork backends.
Conc-anal write-ups, including strace + py-spy evidence:
- `ai/conc-anal/trio_wakeup_socketpair_busy_loop_under_fork_issue.md`
- `ai/conc-anal/infected_asyncio_under_main_thread_forkserver_hang_issue.md`
Regression tests in `tests/trionics/test_patches.py`: each test asserts
(a) the bug exists pre-patch (or is fixed upstream — skip cleanly), (b)
the patch fixes it with a SIGALRM wall-clock cap so a regression hangs
loud instead of silently.
TODO:
- [ ] file the upstream `python-trio/trio` issue + PR.
- [ ] use the `repro()` callable in `_wakeup_socketpair.py` IS the issue
body's evidence section.
(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 0ef549fadb6b95d717457301f3470305dee1f01a)
(factored: dropped spawn-backend-only paths: ai/conc-anal/infected_asyncio_under_main_thread_forkserver_hang_issue.md)
2026-06-10 00:23:26 +00:00
|
|
|
# Apply defensive monkey-patches for upstream `trio`
|
|
|
|
|
# bugs we've encountered while running tractor — see
|
|
|
|
|
# `tractor.trionics.patches` for the catalog +
|
|
|
|
|
# per-patch upstream-fix tracking. Must run BEFORE
|
|
|
|
|
# any trio runtime init.
|
|
|
|
|
from .trionics.patches import apply_all
|
|
|
|
|
apply_all()
|
|
|
|
|
|
2026-06-10 00:16:02 +00:00
|
|
|
subactor = Actor(
|
|
|
|
|
name=uid[0],
|
|
|
|
|
uuid=uid[1],
|
|
|
|
|
loglevel=loglevel,
|
|
|
|
|
spawn_method=spawn_method,
|
|
|
|
|
)
|
2026-05-08 04:04:48 +00:00
|
|
|
|
|
|
|
|
# XXX, set a stable OS-level proc-title BEFORE entering
|
|
|
|
|
# the trio runtime so `ps`/`top`/`acli.pytree` and
|
|
|
|
|
# orphan-reapers can identify this actor for its full
|
|
|
|
|
# lifetime — e.g.
|
|
|
|
|
# `tractor[doggy@1027301b]`
|
|
|
|
|
# vs. the default uninformative
|
|
|
|
|
# `python -m tractor._child --uid (...)`
|
|
|
|
|
#
|
|
|
|
|
# `setproctitle` mutates `argv[0]` (visible in
|
|
|
|
|
# `/proc/<pid>/cmdline`) AND the kernel `comm`
|
|
|
|
|
# (visible in `/proc/<pid>/comm`, kernel-truncated to
|
|
|
|
|
# ~15 bytes, but preserved through zombie state). Both
|
|
|
|
|
# surfaces are enough for `_testing._reap` /
|
|
|
|
|
# `acli.reap` orphan- and zombie-detection to identify
|
|
|
|
|
# tractor sub-actors via intrinsic signals — no cwd,
|
|
|
|
|
# venv path, or env-var coincidence-of-implementation
|
|
|
|
|
# matching needed.
|
|
|
|
|
#
|
|
|
|
|
# NB: an earlier draft also wrote `TRACTOR_AID` to
|
|
|
|
|
# `os.environ` here for `pgrep --env`-style discovery,
|
|
|
|
|
# but Linux snapshots `/proc/<pid>/environ` at exec/fork
|
|
|
|
|
# time, so post-fork runtime mutations don't propagate
|
|
|
|
|
# to the kernel-visible env. The proc-title path
|
|
|
|
|
# provides equivalent ergonomics
|
|
|
|
|
# (`pgrep -f 'tractor\['`) without that gotcha.
|
|
|
|
|
from .devx._proctitle import set_actor_proctitle
|
|
|
|
|
set_actor_proctitle(subactor)
|
|
|
|
|
|
2026-06-10 00:16:02 +00:00
|
|
|
_trio_main(
|
|
|
|
|
subactor,
|
|
|
|
|
parent_addr=parent_addr,
|
|
|
|
|
infect_asyncio=infect_asyncio,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2020-07-20 19:18:38 +00:00
|
|
|
if __name__ == "__main__":
|
2024-04-14 21:49:18 +00:00
|
|
|
__tracebackhide__: bool = True
|
2020-07-27 17:55:37 +00:00
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser()
|
2020-07-28 00:05:00 +00:00
|
|
|
parser.add_argument("--uid", type=parse_uid)
|
|
|
|
|
parser.add_argument("--loglevel", type=str)
|
|
|
|
|
parser.add_argument("--parent_addr", type=parse_ipaddr)
|
2020-07-27 15:03:17 +00:00
|
|
|
parser.add_argument("--asyncio", action='store_true')
|
2020-07-27 17:55:37 +00:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
2026-06-10 00:16:02 +00:00
|
|
|
_actor_child_main(
|
|
|
|
|
uid=args.uid,
|
2020-07-28 00:05:00 +00:00
|
|
|
loglevel=args.loglevel,
|
2020-07-27 15:03:17 +00:00
|
|
|
parent_addr=args.parent_addr,
|
|
|
|
|
infect_asyncio=args.asyncio,
|
2026-06-10 00:16:02 +00:00
|
|
|
spawn_method='trio',
|
2020-07-27 15:03:17 +00:00
|
|
|
)
|