Conversation
Lowkey.config and Lowkey.keys are both mutable state memoized as instance variables on the Lowkey module. Reading either from a non-main Ractor raises Ractor::IsolationError -- Class/Module instance variables can only be read cross-Ractor if their value is frozen. freeze_config! freezes the config, same tradeoff as LowType's own freeze_config! (low-rb/lowtype#49): #configure can no longer mutate it afterward. make_shareable! calls Ractor.make_shareable(keys), deep-freezing and validating the whole file/class/method-proxy registry so it can be read from a worker Ractor. This only works once low_type has stopped leaving a live Binding on class_proxy.class_binding (low-rb/lowtype#50) -- a Binding can never be made Ractor-shareable, even frozen, and would otherwise block this unconditionally for any registry entry produced via LowType.
…es it params_with_expressions is lazily memoized (@params_with_expressions ||= ...) rather than computed in #initialize, because param_proxy#expression isn't populated yet at construction time -- Lowkey.load builds these while parsing; low_type's Evaluator only sets each param's #expression afterward, once the class body has finished loading. Computing it eagerly at construction (tried this first) permanently bakes in an empty result. Left lazy, the first call after Ractor.make_shareable(keys) has frozen the registry raises FrozenError trying to cache its result -- exactly the kind of thing this method exists to prevent for the rest of the registry. make_shareable! now walks every method proxy and calls params_with_expressions once, warming the cache while everything's still mutable, before freezing. Found this via low-rb/low_type's per-class ractor_safe! work (needs params_with_expressions from inside a Ractor) -- without it, any typed/untyped method whose params_with_expressions had never been called yet would raise on first cross-Ractor call.
|
Pushed one more fix found while building low_type's per-class Left as-is, the first call to (Tried computing it eagerly in |
What
Lowkey.configandLowkey.keysare both mutable state memoized as instancevariables on the
Lowkeymodule. Reading either from a non-main Ractor raises:Lowkey.keysis the whole file/class/method-proxy registry everyLowType-included class ends up in, so this blocks calling any typed/untyped method
from a worker Ractor at all.
Fix
Lowkey.freeze_config!-- freezesconfig, same tradeoff asLowType's ownfreeze_config!(Add LowType.freeze_config! for Ractor-safety lowtype#49):#configurecan no longer mutate itafterward.
Lowkey.make_shareable!-- callsRactor.make_shareable(keys), deep-freezingand validating the whole registry so it can be read from a worker Ractor.
Both are meant to be called once, after boot (all
Lowkey.loadcalls done),before spawning any worker Ractors.
Important dependency
make_shareable!only works once low-rb/lowtype#50 lands.class_proxy(stored inside the registry) currently holds a live
Bindinginclass_binding, and aBindingcan never be made Ractor-shareable, evenfrozen:
Without that low_type fix,
make_shareable!raisesRactor::Errorfor anyregistry populated via
LowType.included.Testing
New specs in
spec/units/lowkey_spec.rb:.freeze_config!freezes the config and makes#configureraiseFrozenErrorafterward..make_shareable!-- loads a real fixture, callsmake_shareable!, thenactually spawns a Ractor and reads
Lowkey['spec/fixtures/extend_module.rb']from inside it, asserting it returns the real
FileProxyrather than raising.Full suite: 34 examples, 0 failures. Rubocop clean.
Third of three PRs from a look at what it'd take to make Raindeer's stack
Ractor-safe (see low-rb/lowtype#49 and #50 for the other two).
🤖 Generated with Claude Code