Skip to content

Commit 7804a9f

Browse files
committed
Refactor _runtime_vars into pure get/set API
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
1 parent 63ab7c9 commit 7804a9f

2 files changed

Lines changed: 81 additions & 29 deletions

File tree

tractor/runtime/_state.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,14 @@ def update(
117117
)
118118

119119

120-
_runtime_vars: dict[str, Any] = {
120+
# The "fresh process" defaults — what `_runtime_vars` looks
121+
# like in a just-booted Python process that hasn't yet entered
122+
# `open_root_actor()` nor received a parent `SpawnSpec`. Kept
123+
# as a module-level constant so `get_runtime_vars(clear_values=
124+
# True)` can reset the live dict back to this baseline (see
125+
# `tractor.spawn._subint_forkserver` for the one current caller
126+
# that needs it).
127+
_RUNTIME_VARS_DEFAULTS: dict[str, Any] = {
121128
# root of actor-process tree info
122129
'_is_root': False, # bool
123130
'_root_mailbox': (None, None), # tuple[str|None, str|None]
@@ -138,10 +145,12 @@ def update(
138145
# infected-`asyncio`-mode: `trio` running as guest.
139146
'_is_infected_aio': False,
140147
}
148+
_runtime_vars: dict[str, Any] = dict(_RUNTIME_VARS_DEFAULTS)
141149

142150

143151
def get_runtime_vars(
144152
as_dict: bool = True,
153+
clear_values: bool = False,
145154
) -> dict:
146155
'''
147156
Deliver a **copy** of the current `Actor`'s "runtime variables".
@@ -150,11 +159,62 @@ def get_runtime_vars(
150159
form, but the `RuntimeVars` struct should be utilized as possible
151160
for future calls.
152161
162+
Pure read — **never mutates** the module-level `_runtime_vars`.
163+
164+
If `clear_values=True`, return a copy of the fresh-process
165+
defaults (`_RUNTIME_VARS_DEFAULTS`) instead of the live
166+
dict. Useful in combination with `set_runtime_vars()` to
167+
reset process-global state back to "cold" — the main caller
168+
today is the `subint_forkserver` spawn backend's post-fork
169+
child prelude:
170+
171+
set_runtime_vars(get_runtime_vars(clear_values=True))
172+
173+
`os.fork()` inherits the parent's full memory image, so the
174+
child sees the parent's populated `_runtime_vars` (e.g.
175+
`_is_root=True`) which would trip the `assert not
176+
self.enable_modules` gate in `Actor._from_parent()` on the
177+
subsequent parent→child `SpawnSpec` handshake if left alone.
178+
153179
'''
180+
src: dict = (
181+
_RUNTIME_VARS_DEFAULTS
182+
if clear_values
183+
else _runtime_vars
184+
)
185+
snapshot: dict = dict(src)
154186
if as_dict:
155-
return dict(_runtime_vars)
187+
return snapshot
188+
return RuntimeVars(**snapshot)
189+
156190

157-
return RuntimeVars(**_runtime_vars)
191+
def set_runtime_vars(
192+
rtvars: dict | RuntimeVars,
193+
) -> None:
194+
'''
195+
Atomically replace the module-level `_runtime_vars` contents
196+
with those of `rtvars` (via `.clear()` + `.update()` so
197+
live references to the same dict object remain valid).
198+
199+
Accepts either the historical `dict` form or the `RuntimeVars`
200+
`msgspec.Struct` form (the latter still mostly unused but
201+
the blessed forward shape — see the struct's definition).
202+
203+
Paired with `get_runtime_vars()` as the explicit
204+
write-half of the runtime-vars API — prefer this over
205+
direct mutation of `_runtime_vars[...]` from new call sites.
206+
207+
'''
208+
if isinstance(rtvars, RuntimeVars):
209+
# `msgspec.Struct` → dict via its declared field set;
210+
# avoids pulling in `msgspec.structs.asdict` just for
211+
# this one call path.
212+
rtvars = {
213+
field_name: getattr(rtvars, field_name)
214+
for field_name in rtvars.__struct_fields__
215+
}
216+
_runtime_vars.clear()
217+
_runtime_vars.update(rtvars)
158218

159219

160220
def last_actor() -> Actor|None:

tractor/spawn/_subint_forkserver.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -548,32 +548,24 @@ def _child_target() -> int:
548548
# every spawn — it's module-level in `_child` but
549549
# cheap enough to re-resolve here.
550550
from tractor._child import _actor_child_main
551-
# XXX, fork inherits the parent's entire memory
552-
# image — including `tractor.runtime._state` globals
553-
# that encode "this process is the root actor":
554-
#
555-
# - `_runtime_vars['_is_root']` → True in parent
556-
# - pre-populated `_root_mailbox`, `_registry_addrs`
557-
# - the parent's `_current_actor` singleton
558-
#
559-
# A fresh `exec`-based child would start with the
560-
# `_state` module's defaults (all falsey / empty).
561-
# Replicate that here so the new child-side `Actor`
562-
# sees a "cold" runtime — otherwise `Actor.__init__`
563-
# takes the `is_root_process() == True` branch and
564-
# pre-populates `self.enable_modules`, which then
565-
# trips the `assert not self.enable_modules` gate at
566-
# the top of `Actor._from_parent()` on the subsequent
567-
# parent→child `SpawnSpec` handshake.
568-
from tractor.runtime import _state
569-
_state._current_actor = None
570-
_state._runtime_vars.update({
571-
'_is_root': False,
572-
'_root_mailbox': (None, None),
573-
'_root_addrs': [],
574-
'_registry_addrs': [],
575-
'_debug_mode': False,
576-
})
551+
# XXX, `os.fork()` inherits the parent's entire memory
552+
# image, including `tractor.runtime._state._runtime_vars`
553+
# (which in the parent encodes "this process IS the root
554+
# actor"). A fresh `exec`-based child starts cold; we
555+
# replicate that here by explicitly resetting runtime
556+
# vars to their fresh-process defaults — otherwise
557+
# `Actor.__init__` takes the `is_root_process() == True`
558+
# branch, pre-populates `self.enable_modules`, and trips
559+
# the `assert not self.enable_modules` gate at the top
560+
# of `Actor._from_parent()` on the subsequent parent→
561+
# child `SpawnSpec` handshake. (`_state._current_actor`
562+
# is unconditionally overwritten by `_trio_main` → no
563+
# reset needed for it.)
564+
from tractor.runtime._state import (
565+
get_runtime_vars,
566+
set_runtime_vars,
567+
)
568+
set_runtime_vars(get_runtime_vars(clear_values=True))
577569
_actor_child_main(
578570
uid=uid,
579571
loglevel=loglevel,

0 commit comments

Comments
 (0)