Reset post-fork `_state` in forkserver child

`os.fork()` inherits the parent's entire memory image,
including `tractor.runtime._state` globals that encode
"this process is the root actor" — `_runtime_vars`'s
`_is_root=True`, pre-populated `_root_mailbox` +
`_registry_addrs`, and the parent's `_current_actor`
singleton.

A fresh `exec`-based child starts with those globals at
their module-level defaults (all falsey/empty). The
forkserver child needs to match that shape BEFORE calling
`_actor_child_main()`, otherwise `Actor.__init__()` takes
the `is_root_process() == True` branch and pre-populates
`self.enable_modules`, which then trips
`assert not self.enable_modules` at the top of
`Actor._from_parent()` on the subsequent parent→child
`SpawnSpec` handshake.

Fix: at the start of `_child_target`, null
`_state._current_actor` and overwrite `_runtime_vars` with
a cold-root blank (`_is_root=False`, empty mailbox/addrs,
`_debug_mode=False`) before `_actor_child_main()` runs.

Found-via: `test_subint_forkserver_spawn_basic` hitting
the `enable_modules` assert on child-side runtime boot.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
subint_forkserver_backend
Gud Boi 2026-04-22 19:01:27 -04:00
parent 26914fde75
commit 63ab7c986b
1 changed files with 26 additions and 0 deletions

View File

@ -548,6 +548,32 @@ async def subint_forkserver_proc(
# every spawn — it's module-level in `_child` but # every spawn — it's module-level in `_child` but
# cheap enough to re-resolve here. # cheap enough to re-resolve here.
from tractor._child import _actor_child_main from tractor._child import _actor_child_main
# XXX, fork inherits the parent's entire memory
# image — including `tractor.runtime._state` globals
# that encode "this process is the root actor":
#
# - `_runtime_vars['_is_root']` → True in parent
# - pre-populated `_root_mailbox`, `_registry_addrs`
# - the parent's `_current_actor` singleton
#
# A fresh `exec`-based child would start with the
# `_state` module's defaults (all falsey / empty).
# Replicate that here so the new child-side `Actor`
# sees a "cold" runtime — otherwise `Actor.__init__`
# takes the `is_root_process() == True` branch and
# pre-populates `self.enable_modules`, which then
# trips the `assert not self.enable_modules` gate at
# the top of `Actor._from_parent()` on the subsequent
# parent→child `SpawnSpec` handshake.
from tractor.runtime import _state
_state._current_actor = None
_state._runtime_vars.update({
'_is_root': False,
'_root_mailbox': (None, None),
'_root_addrs': [],
'_registry_addrs': [],
'_debug_mode': False,
})
_actor_child_main( _actor_child_main(
uid=uid, uid=uid,
loglevel=loglevel, loglevel=loglevel,