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

Skip to content

Add per-class ractor_safe! opt-in for Redefiner's method codegen - #51

Open
maedi wants to merge 3 commits into
mainfrom
feature/ractor-safe-per-class-opt-in
Open

Add per-class ractor_safe! opt-in for Redefiner's method codegen#51
maedi wants to merge 3 commits into
mainfrom
feature/ractor-safe-per-class-opt-in

Conversation

@maedi

@maedi maedi commented Sep 8, 2026

Copy link
Copy Markdown
Member

What

Redefiner.typed_methods/untyped_methods wrap every method via
define_method blocks closing over method_proxy. I confirmed empirically
that this makes the resulting method uncallable from any Ractor other than
the one that defined it, regardless of whether the closure captures
anything, and regardless of whether captured state is frozen
:

wrapper = Module.new do
  define_method(:greet) { |name| super(name.upcase) } # zero outer captures
end
Target.prepend(wrapper)

Ractor.new { Target.new.greet('hi') }.take
# => RuntimeError: defined with an un-shareable Proc in a different Ractor

A method compiled from a source string via class_eval doesn't have this
restriction -- it's a real, ISeq-backed method, not a Proc:

wrapper.class_eval(<<~RUBY)
  def greet(name)
    proxy = REGISTRY[:greet]  # shareable lookup, not a closure
    super(proxy.validate ? name.upcase : name)
  end
RUBY
# callable from any Ractor, confirmed

Fix

ractor_safe! in a class body (extended via the new Low::RactorSafety
mixin) opts that one class into the class_eval-string codegen path
(Redefiner.define_ractor_safe_typed_method/_untyped_method). Every other
class keeps the existing define_method path completely unchanged. The
ractor-safe path re-derives method_proxy from Lowkey's registry on every
call (Lowkey[file_path][namespace][__method__]) instead of closing over it
-- the exact per-call lookup that was removed as a redundant allocation in
untyped_methods a while back. That's the real tradeoff: Ractor-callability
costs back some of that allocation win, but only for classes that actually
opt in, not globally.

Dependencies

This only actually works end-to-end once:

This PR's own spec simulates #50 inline (clears class_binding manually in
a throwaway subprocess) so its tests pass standalone regardless of merge
order, but real usage needs all three.

Testing

  • Full suite: 145 examples, 0 failures (up from 141) -- ran the
    pre-existing 141 first, confirmed 0 regressions, before writing anything
    ractor-safe-specific.
  • New fixtures (spec/fixtures/ractor_safe_{typed,untyped}.rb) +
    spec/features/ractor_safety_spec.rb:
    • Confirms validation behavior is identical to the non-ractor-safe path
      (a bad arg still raises Low::ArgumentTypeError; type-checking-disabled
      still passes through unvalidated) -- ractor_safe! changes codegen
      mechanism only, not behavior.
    • Separately spawns a real Ractor in a subprocess (so the shared suite
      process's Lowkey.keys registry is never touched -- freezing it
      in-process would break any later spec expecting to load a new fixture)
      and confirms the generated method is actually callable from it, for both
      the typed and untyped codegen branches.
  • Rubocop clean bar the same pre-existing, unrelated Metrics/AbcSize
    offense on .included already on main. Added rubocop:disable Style/EvalWithLocation around the two class_eval calls -- deliberately
    attributing generated methods to the app's own file/line, not
    redefiner.rb's __FILE__, so a validation error's backtrace points at
    the user's actual method definition.

Third and last of three PRs from a look at what it'd take to make
Raindeer's stack Ractor-safe (see #49 and #50 for the other two, and
low-rb/lowkey#12).

🤖 Generated with Claude Code

maedi and others added 3 commits August 17, 2026 11:09
Redefiner.typed_methods/untyped_methods wrap every method via
define_method blocks closing over method_proxy. Confirmed empirically
that this makes the resulting method uncallable from any Ractor other
than the one that defined it -- Ruby refuses to invoke a Proc-backed
method cross-Ractor, even with zero closure captures and even when
everything captured is frozen:

    RuntimeError: defined with an un-shareable Proc in a different Ractor

A method compiled from a source string via class_eval has no such
restriction -- it's a real method, not Proc-backed -- but a string
can't close over a local variable, so it re-derives method_proxy from
Lowkey's registry on every call instead (Lowkey[file_path][namespace]
[__method__], the same lookup untyped_methods used to do per-call
before that was removed as a redundant allocation).

`ractor_safe!` in a class body opts that one class into the
class_eval-string codegen path; every other class keeps the fast
define_method path unchanged. Most classes never run inside a worker
Ractor and shouldn't pay for a per-call registry lookup they don't
need -- this makes Ractor-compatibility something you ask for per
class, not a global tradeoff everyone pays.

Only actually callable from a worker Ractor once Lowkey.make_shareable!
has been called (low-rb/lowkey#12) and class_proxy.class_binding has
been cleared (#50) -- a live Binding can never be made
Ractor-shareable, even frozen.

Testing:
- Full suite: 145 examples, 0 failures (up from 141 -- existing
  define_method path completely unchanged, verified before writing any
  of this).
- New fixtures (spec/fixtures/ractor_safe_{typed,untyped}.rb) +
  spec/features/ractor_safety_spec.rb: confirms validation behavior is
  identical to the non-ractor-safe path, and separately spawns a real
  Ractor (in a subprocess, so the shared suite process's Lowkey.keys is
  never touched) proving the generated method is actually callable from
  it, for both the typed and untyped codegen branches.
- Rubocop clean bar the same pre-existing, unrelated Metrics/AbcSize
  offense on .included already present on main.

Third and last of three PRs from a look at what it'd take to make
Raindeer's stack Ractor-safe (see #49 and #50 for the other two, and
low-rb/lowkey#12).
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