autosave association callbacks Ractor‑safe に改修
Rails の autosave 連携コールバックが Ractor 環境でも安全に呼び出せるよう、内部実装が変更されました。define_non_cyclic_method が define_method から class_eval へ置き換えられ、ブロックによる Proc 捕捉が排除されました。
背景
Ractor 互換性の欠如 が本変更の直接的な動機です。従来の実装では define_non_cyclic_method が define_method とブロックを使い、非共有 Proc を内部に保持していました。そのため、メイン Ractor 以外からコールバックを実行しようとすると例外が発生していました。PR #57888 ではこの制約を解消し、マルチ‑Ractor アプリケーションでも autosave が期待通りに動作することを目指しました。
この背景は、Rails が並行実行モデルとして Ractor を正式にサポートする方針と一致しており、既存コードの後方互換性を保ちつつ安全性を高める必要があった点がポイントです。
技術的な変更
activerecord/lib/active_record/autosave_association.rb における define_non_cyclic_method の実装が根本的に刷新されました。変更前は次のように define_method とブロックでメソッドを定義していました。
def define_non_cyclic_method(name, &block)
return if method_defined?(name, false)
define_method(name) do |*args|
result = true; @_already_called ||= {}
unless @_already_called[name]
begin
@_already_called[name] = true
result = instance_eval(&block)
ensure
@_already_called[name] = false
end
end
result
end
end
変更後は class_eval とシンボル引数を用いてメソッドを生成します。これによりブロックが不要となり、Ractor 間で安全に共有できるコードに置き換わります。
def define_non_cyclic_method(name, reflection, method)
return if method_defined?(name, false)
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{name}(*args)
result = true; @_already_called ||= {}
unless @_already_called[:#{name}]
reflection = self.class._reflect_on_association(:#{reflection.name})
if reflection
begin
@_already_called[:#{name}] = true
result = #{method}(reflection)
ensure
@_already_called[:#{name}] = false
end
end
end
result
end
RUBY
end
この実装は、reflection オブジェクトを明示的に取得して対象メソッドへ引数として渡す構造に変更し、_already_called フラグのキーをシンボル化する点で差異があります。結果として、define_method が生成した閉じたブロックが無くなり、Ractor が要求する「共有可能な」オブジェクトだけが残ります。
設計判断
class_eval を選択した設計 は、既存のインターフェースを保ちつつ Ractor 互換性を確保するための最小侵襲なアプローチです。PR の議論では別のキー define_non_cyclic_method のシグネチャ変更や新規ヘルパーの追加案も検討されましたが、メソッド名や呼び出し側コードを変更せずに済む点が評価されました。
また、reflection と method をシンボルで受け渡すことで、メソッド内部で再度 self.class._reflect_on_association を呼び出す余計な依存を排除し、実行時のオブジェクト生成コストを抑えています。これにより、Ractor が内部的にコピーやシリアライズを行う際の負荷も低減されます。
まとめ
本 PR は autosave_association.rb のコールバック定義を Ractor‑安全な形に改修し、Rails アプリケーションがマルチ‑Ractor 環境で autosave を正しく利用できるようにしました。実装変更は define_non_cyclic_method の内部ロジックに留まり、外部 API への影響はありません。