From 7804a9feac1e728b5bc12bf74ebd8f45c73ba4e0 Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 22 Apr 2026 19:10:06 -0400 Subject: [PATCH] Refactor `_runtime_vars` into pure get/set API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-fork `_runtime_vars` reset in `subint_forkserver_proc` was previously done via direct mutation of `_state._runtime_vars` from an external module + an inline default dict duplicating the `_state.py`-internal defaults. Split the access surface into a pure getter + explicit setter so the reset call site becomes a one-liner composition. Deats `tractor/runtime/_state.py`, - extract initial values into a module-level `_RUNTIME_VARS_DEFAULTS: dict[str, Any]` constant; the live `_runtime_vars` is now initialised from `dict(_RUNTIME_VARS_DEFAULTS)` - `get_runtime_vars()` grows a `clear_values: bool = False` kwarg. When True, returns a fresh copy of `_RUNTIME_VARS_DEFAULTS` instead of the live dict — still a **pure read**, never mutates anything - new `set_runtime_vars(rtvars: dict | RuntimeVars)` — atomic replacement of the live dict's contents via `.clear()` + `.update()`, so existing references to the same dict object remain valid. Accepts either the historical dict form or the `RuntimeVars` struct Deats `tractor/spawn/_subint_forkserver.py`, - collapse the prior ad-hoc `.update({...})` block into `set_runtime_vars(get_runtime_vars(clear_values=True))` - drop the `_state._current_actor = None` line — `_trio_main` unconditionally overwrites it downstream, so no explicit reset needed (noted in the XXX comment) (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/runtime/_state.py | 70 ++++++++++++++++++++++++++--- tractor/spawn/_subint_forkserver.py | 44 ++++++++---------- 2 files changed, 83 insertions(+), 31 deletions(-) diff --git a/tractor/runtime/_state.py b/tractor/runtime/_state.py index 55aa3291..aedcc952 100644 --- a/tractor/runtime/_state.py +++ b/tractor/runtime/_state.py @@ -117,7 +117,14 @@ class RuntimeVars(Struct): ) -_runtime_vars: dict[str, Any] = { +# The "fresh process" defaults — what `_runtime_vars` looks +# like in a just-booted Python process that hasn't yet entered +# `open_root_actor()` nor received a parent `SpawnSpec`. Kept +# as a module-level constant so `get_runtime_vars(clear_values= +# True)` can reset the live dict back to this baseline (see +# `tractor.spawn._subint_forkserver` for the one current caller +# that needs it). +_RUNTIME_VARS_DEFAULTS: dict[str, Any] = { # root of actor-process tree info '_is_root': False, # bool '_root_mailbox': (None, None), # tuple[str|None, str|None] @@ -138,10 +145,12 @@ _runtime_vars: dict[str, Any] = { # infected-`asyncio`-mode: `trio` running as guest. '_is_infected_aio': False, } +_runtime_vars: dict[str, Any] = dict(_RUNTIME_VARS_DEFAULTS) def get_runtime_vars( as_dict: bool = True, + clear_values: bool = False, ) -> dict: ''' Deliver a **copy** of the current `Actor`'s "runtime variables". @@ -150,11 +159,62 @@ def get_runtime_vars( form, but the `RuntimeVars` struct should be utilized as possible for future calls. - ''' - if as_dict: - return dict(_runtime_vars) + Pure read — **never mutates** the module-level `_runtime_vars`. - return RuntimeVars(**_runtime_vars) + If `clear_values=True`, return a copy of the fresh-process + defaults (`_RUNTIME_VARS_DEFAULTS`) instead of the live + dict. Useful in combination with `set_runtime_vars()` to + reset process-global state back to "cold" — the main caller + today is the `subint_forkserver` spawn backend's post-fork + child prelude: + + set_runtime_vars(get_runtime_vars(clear_values=True)) + + `os.fork()` inherits the parent's full memory image, so the + child sees the parent's populated `_runtime_vars` (e.g. + `_is_root=True`) which would trip the `assert not + self.enable_modules` gate in `Actor._from_parent()` on the + subsequent parent→child `SpawnSpec` handshake if left alone. + + ''' + src: dict = ( + _RUNTIME_VARS_DEFAULTS + if clear_values + else _runtime_vars + ) + snapshot: dict = dict(src) + if as_dict: + return snapshot + return RuntimeVars(**snapshot) + + +def set_runtime_vars( + rtvars: dict | RuntimeVars, +) -> None: + ''' + Atomically replace the module-level `_runtime_vars` contents + with those of `rtvars` (via `.clear()` + `.update()` so + live references to the same dict object remain valid). + + Accepts either the historical `dict` form or the `RuntimeVars` + `msgspec.Struct` form (the latter still mostly unused but + the blessed forward shape — see the struct's definition). + + Paired with `get_runtime_vars()` as the explicit + write-half of the runtime-vars API — prefer this over + direct mutation of `_runtime_vars[...]` from new call sites. + + ''' + if isinstance(rtvars, RuntimeVars): + # `msgspec.Struct` → dict via its declared field set; + # avoids pulling in `msgspec.structs.asdict` just for + # this one call path. + rtvars = { + field_name: getattr(rtvars, field_name) + for field_name in rtvars.__struct_fields__ + } + _runtime_vars.clear() + _runtime_vars.update(rtvars) def last_actor() -> Actor|None: diff --git a/tractor/spawn/_subint_forkserver.py b/tractor/spawn/_subint_forkserver.py index bf3f3789..ea795c8c 100644 --- a/tractor/spawn/_subint_forkserver.py +++ b/tractor/spawn/_subint_forkserver.py @@ -548,32 +548,24 @@ async def subint_forkserver_proc( # every spawn — it's module-level in `_child` but # cheap enough to re-resolve here. 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, - }) + # XXX, `os.fork()` inherits the parent's entire memory + # image, including `tractor.runtime._state._runtime_vars` + # (which in the parent encodes "this process IS the root + # actor"). A fresh `exec`-based child starts cold; we + # replicate that here by explicitly resetting runtime + # vars to their fresh-process defaults — otherwise + # `Actor.__init__` takes the `is_root_process() == True` + # branch, pre-populates `self.enable_modules`, and trips + # the `assert not self.enable_modules` gate at the top + # of `Actor._from_parent()` on the subsequent parent→ + # child `SpawnSpec` handshake. (`_state._current_actor` + # is unconditionally overwritten by `_trio_main` → no + # reset needed for it.) + from tractor.runtime._state import ( + get_runtime_vars, + set_runtime_vars, + ) + set_runtime_vars(get_runtime_vars(clear_values=True)) _actor_child_main( uid=uid, loglevel=loglevel,