PROTECTED_IVARS 定数を freeze して Ractor セーフにする
Rails の内部で使用される PROTECTED_IVARS 定数に .freeze を付与し、Ractor 環境で安全に共有できるようにしました。
背景
PROTECTED_IVARS はビューへ流出させてはいけないインスタンス変数名のリストを保持しており、ActionMailer と ActionController の基底クラスで定義されています。これらは配列リテラルとして生成されるため変更可能であり、Ractor が要求する「不変(immutable)であること」を満たしていませんでした。Rubocop の指摘でこの点が見落とされていたことが判明し、Ractor に対応するための修正が必要となりました。
技術的な変更
actionmailer/lib/action_mailer/base.rb と actionpack/lib/action_controller/base.rb の PROTECTED_IVARS 定数定義の末尾に .freeze を付与しました。これにより、配列オブジェクトが生成時に凍結され、以降の変更が禁止されます。変更前後のコードは以下の通りです。
変更前:
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]
変更後:
PROTECTED_IVARS = (AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]).freeze
変更前:
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + %i(
@_params @_response @_request @_config @_url_options @_action_has_layout @_view_context_class
@_view_renderer @_lookup_context @_routes @_view_runtime @_db_runtime @_helper_proxy
@_marked_for_same_origin_verification @_verify_authenticity_token_ran @_rendered_format
)
変更後:
PROTECTED_IVARS = (AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + %i(
@_params @_response @_request @_config @_url_options @_action_has_layout @_view_context_class
@_view_renderer @_lookup_context @_routes @_view_runtime @_db_runtime @_helper_proxy
@_marked_for_same_origin_verification @_verify_authenticity_token_ran @_rendered_format
)).freeze
設計判断
freeze を利用した不変化は、既存の API を変更せずに安全性を向上させる最小限の侵入策です。新たにラッパークラスや深いコピーを導入するとパフォーマンスと可読性に影響が出る可能性がありますが、単に定数リテラルを凍結するだけで Ractor の共有オブジェクト要件を満たします。また、true/false のようなフラグと同様に定数は読み取り専用として扱われていたため、後方互換性に問題は生じません。
まとめ
PROTECTED_IVARS を凍結することで、ActionMailer と ActionController が内部で保持する保護対象変数リストが Ractor 環境でも安全に共有可能になり、マルチスレッド・マルチプロセスの実行モデルへの適合性が向上しました。最小限のコード変更で済むこのアプローチは、Rails の設計哲学である「シンプルで破壊的でない拡張」を体現しています。