Skip to content

Commit dcd5c1f

Browse files
committed
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
1 parent 76605d5 commit dcd5c1f

1 file changed

Lines changed: 81 additions & 0 deletions

File tree

tractor/spawn/_subint_forkserver.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@
6666
- no cancellation / hard-kill stress coverage yet (counterpart
6767
to `tests/test_subint_cancellation.py` for the plain
6868
`subint` backend),
69+
- `child_sigint='trio'` mode (flag scaffolded below; default
70+
is `'ipc'`): install a manual SIGINT → trio-cancel bridge
71+
in the fork-child prelude so externally-delivered SIGINT
72+
reaches the child's trio loop even when the parent is
73+
dead (no IPC cancel path). See the xfail'd
74+
`test_orphaned_subactor_sigint_cleanup_DRAFT` for the
75+
target behavior.
6976
- child-side "subint-hosted root runtime" mode (the second
7077
half of the envisioned arch — currently the forked child
7178
runs plain `_trio_main` via `spawn_method='trio'`; the
@@ -115,6 +122,7 @@
115122
from typing import (
116123
Any,
117124
Callable,
125+
Literal,
118126
TYPE_CHECKING,
119127
)
120128

@@ -145,6 +153,31 @@
145153
log = get_logger('tractor')
146154

147155

156+
# Configurable child-side SIGINT handling for forkserver-spawned
157+
# subactors. Threaded through `subint_forkserver_proc`'s
158+
# `proc_kwargs` under the `'child_sigint'` key.
159+
#
160+
# - `'ipc'` (default, currently the only implemented mode):
161+
# child has NO trio-level SIGINT handler — trio.run() is on
162+
# the fork-inherited non-main thread, `signal.set_wakeup_fd()`
163+
# is main-thread-only. Cancellation flows exclusively via
164+
# the parent's `Portal.cancel_actor()` IPC path. Safe +
165+
# deterministic for nursery-structured apps where the parent
166+
# is always the cancel authority. Known gap: orphan
167+
# (post-parent-SIGKILL) children don't respond to SIGINT
168+
# — see `test_orphaned_subactor_sigint_cleanup_DRAFT`.
169+
#
170+
# - `'trio'` (**not yet implemented**): install a manual
171+
# SIGINT → trio-cancel bridge in the child's fork prelude
172+
# (pre-`trio.run()`) so external Ctrl-C reaches stuck
173+
# grandchildren even with a dead parent. Adds signal-
174+
# handling surface the `'ipc'` default cleanly avoids; only
175+
# pay for it when externally-interruptible children actually
176+
# matter (e.g. CLI tool grandchildren).
177+
ChildSigintMode = Literal['ipc', 'trio']
178+
_DEFAULT_CHILD_SIGINT: ChildSigintMode = 'ipc'
179+
180+
148181
# Feature-gate: py3.14+ via the public `concurrent.interpreters`
149182
# wrapper. Matches the gate in `tractor.spawn._subint` —
150183
# see that module's docstring for why we require the public
@@ -537,13 +570,61 @@ async def subint_forkserver_proc(
537570
f'Current runtime: {sys.version}'
538571
)
539572

573+
# Backend-scoped config pulled from `proc_kwargs`. Using
574+
# `proc_kwargs` (vs a first-class kwarg on this function)
575+
# matches how other backends expose per-spawn tuning
576+
# (`trio_proc` threads it to `trio.lowlevel.open_process`,
577+
# etc.) and keeps `ActorNursery.start_actor(proc_kwargs=...)`
578+
# as the single ergonomic entry point.
579+
child_sigint: ChildSigintMode = proc_kwargs.get(
580+
'child_sigint',
581+
_DEFAULT_CHILD_SIGINT,
582+
)
583+
if child_sigint not in ('ipc', 'trio'):
584+
raise ValueError(
585+
f'Invalid `child_sigint={child_sigint!r}` for '
586+
f'`subint_forkserver` backend.\n'
587+
f'Expected one of: {ChildSigintMode}.'
588+
)
589+
if child_sigint == 'trio':
590+
raise NotImplementedError(
591+
f"`child_sigint='trio'` mode — trio-native SIGINT "
592+
f"plumbing in the fork-child — is scaffolded but "
593+
f"not yet implemented. See the xfail'd "
594+
f"`test_orphaned_subactor_sigint_cleanup_DRAFT` "
595+
f"and the TODO in this module's docstring."
596+
)
597+
540598
uid: tuple[str, str] = subactor.aid.uid
541599
loglevel: str | None = subactor.loglevel
542600

543601
# Closure captured into the fork-child's memory image.
544602
# In the child this is the first post-fork Python code to
545603
# run, on what was the fork-worker thread in the parent.
604+
# `child_sigint` is captured here so the impl lands inside
605+
# this function once the `'trio'` mode is wired up —
606+
# nothing above this comment needs to change.
546607
def _child_target() -> int:
608+
# Dispatch on the captured SIGINT-mode closure var.
609+
# Today only `'ipc'` is reachable (the `'trio'` branch
610+
# is fenced off at the backend-entry guard above); the
611+
# match is in place so the future `'trio'` impl slots
612+
# in as a plain case arm without restructuring.
613+
match child_sigint:
614+
case 'ipc':
615+
pass # <- current behavior: no child-side
616+
# SIGINT plumbing; rely on parent
617+
# `Portal.cancel_actor()` IPC path.
618+
case 'trio':
619+
# Unreachable today (see entry-guard above);
620+
# this stub exists so that lifting the guard
621+
# is the only change required to enable
622+
# `'trio'` mode once the SIGINT wakeup-fd
623+
# bridge is implemented.
624+
raise NotImplementedError(
625+
"`child_sigint='trio'` fork-prelude "
626+
"plumbing not yet wired."
627+
)
547628
# Lazy import so the parent doesn't pay for it on
548629
# every spawn — it's module-level in `_child` but
549630
# cheap enough to re-resolve here.

0 commit comments

Comments
 (0)