Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add Lowkey.freeze_config! and .make_shareable! for Ractor-safety - #12

Open
maedi wants to merge 2 commits into
mainfrom
feature/ractor-safe-registry
Open

maedi wants to merge 2 commits into
mainfrom
feature/ractor-safe-registry

Conversation

@maedi

@maedi maedi commented Sep 8, 2026

Copy link
Copy Markdown
Member

What

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: can not get unshareable values from instance variables
of classes/modules from non-main Ractors

Lowkey.keys is the whole file/class/method-proxy registry every LowType-
included class ends up in, so this blocks calling any typed/untyped method
from a worker Ractor at all.

Fix

  • Lowkey.freeze_config! -- freezes config, same tradeoff as LowType's own
    freeze_config! (Add LowType.freeze_config! for Ractor-safety lowtype#49): #configure can no longer mutate it
    afterward.
  • Lowkey.make_shareable! -- calls Ractor.make_shareable(keys), deep-freezing
    and validating the whole registry so it can be read from a worker Ractor.

Both are meant to be called once, after boot (all Lowkey.load calls 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 Binding in
class_binding, and a Binding can never be made Ractor-shareable, even
frozen:

b = binding
b.freeze
Ractor.make_shareable(b)
# => Ractor::Error: can not make shareable object for #<Binding:0x...>

Without that low_type fix, make_shareable! raises Ractor::Error for any
registry populated via LowType.included.

Testing

New specs in spec/units/lowkey_spec.rb:

  • .freeze_config! freezes the config and makes #configure raise
    FrozenError afterward.
  • .make_shareable! -- loads a real fixture, calls make_shareable!, then
    actually spawns a Ractor and reads Lowkey['spec/fixtures/extend_module.rb']
    from inside it, asserting it returns the real FileProxy rather 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

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.
@maedi

maedi commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

Pushed one more fix found while building low_type's per-class ractor_safe!
opt-in (low-rb/lowtype#51, incoming): MethodProxy#params_with_expressions
is lazily memoized (@params_with_expressions ||= ...), 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 finishes loading.

Left as-is, the first call to params_with_expressions after
make_shareable! has frozen the registry raises FrozenError trying to
cache its result. make_shareable! now walks every method proxy and calls
it once first, while everything's still mutable, so the cache is already
warm by the time the freeze happens.

(Tried computing it eagerly in #initialize first -- that's wrong, since
#expression isn't set yet at that point and it permanently bakes in an
empty result. Reverted that, kept the lazy method, just warm it explicitly.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant