From fc049abe2a1b6d21d9347cea71295ef5a9dda08a Mon Sep 17 00:00:00 2001 From: goodboy Date: Tue, 9 Jun 2026 20:19:56 -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 Resetting `_runtime_vars` post-(forking-)spawn was previously only possible 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 such a reset call site becomes a one-liner composition: `set_runtime_vars(get_runtime_vars(clear_values=True))`. 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 (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 7804a9fe57693dd5e15bee6a08e7d2fa14b6a98a) (factored: kept only the tractor/runtime/_state.py part; dropped tractor/spawn/_subint_forkserver.py call-site rewire) --- tractor/runtime/_state.py | 70 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 5 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: