Active Record の定数を Ractor セーフにする修正
この PR は、Active Record と Arel が内部で保持している proc 定数 を ActiveSupport::Ractors.shareable_proc でラップし、Ractor 間で安全に共有できるようにします。挙動は変わらず、Ractor 未使用環境でも従来通りに動作します。
背景
Rails 7 以降で導入された Ractor は、オブジェクトの共有に厳格な制限を課します。そのため、プロシージャやラムダのような クロージャ が外部状態を捕捉しない限り、shareable_proc で包むことで安全に共有できます。Active Record の内部では、REJECT_ALL_BLANK_PROC や BIND_BLOCK といった proc 定数 が複数箇所で利用されており、Ractor 環境での利用を阻害していました。これらの定数は外部状態を参照せず、引数だけで完結するため、Ractor セーフ化の対象として最適です。
技術的な変更
activerecord/lib/active_record/nested_attributes.rb では、REJECT_ALL_BLANK_PROC が proc から ActiveSupport::Ractors.shareable_proc に置き換えられました。
@@ -300,7 +300,7 @@ module ClassMethods
- REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } }
+ REJECT_ALL_BLANK_PROC = ActiveSupport::Ractors.shareable_proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } }
activerecord/lib/arel/visitors/postgresql.rb では、バインド変数のフォーマットを行う BIND_BLOCK が同様にラップされました。
@@ -140,7 +140,7 @@ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
- BIND_BLOCK = proc { |i| "$#{i}" }
+ BIND_BLOCK = ActiveSupport::Ractors.shareable_proc { |i| "$#{i}" }
private_constant :BIND_BLOCK
activerecord/lib/arel/visitors/to_sql.rb でも、プレースホルダー用の BIND_BLOCK が shareable_proc に変更されています。
@@ -765,7 +765,7 @@ def visit_Arel_Attributes_Attribute(o, collector)
- BIND_BLOCK = proc { "?" }
+ BIND_BLOCK = ActiveSupport::Ractors.shareable_proc { "?" }
private_constant :BIND_BLOCK
これらの変更に伴い、bind_block メソッドは依然として同じ定数を返すだけで、呼び出し側のロジックは一切変わりません。プロシージャ自体が外部状態を捕捉しないため、shareable_proc に置き換えても機能上の差異はありません。
設計判断
PR では 既存定数をそのまま拡張 する方針が採用されました。新たに別名の定数や設定オプションを導入する代わりに、ActiveSupport::Ractors.shareable_proc でラップするだけの最小限の変更にとどめたことで、既存コードとの互換性が保たれます。さらに、private_constant が維持されるため、外部からの直接参照が制限された状態で安全に共有できます。これは、Ractor 対応を段階的に進める上で、リスクを抑えつつ API の破壊を避ける意図的なトレードオフと言えます。
まとめ
本変更は、Active Record と Arel が内部で使用する 3 つの proc 定数 を Ractor セーフにし、将来的なマルチスレッド/マルチプロセス実行環境への備えを整えました。動作上の変更はなく、既存アプリケーションへの影響もありません。Ractor 対応を進める上で、最小限の侵入性で安全性を確保した設計判断が示されています。