定数の Ractor 共有化のための freeze と shareable ヘルパー追加
Rails は Ractor 対応を進める中で、定数に保持された可変オブジェクトが Ractor 間で安全に共有できない問題を解消するため、数箇所で freeze と shareable ヘルパーを導入しました。これにより、挙動を変えることなく Ractor‑safe な実装が可能になります。
背景
Ractor へ移行する際の最重要課題は、共有メモリ上のオブジェクトが不変であることを保証することです。フレームワーク内部で定数として保持されているハッシュや Proc はデフォルトでミュータブルであり、Ractor 間で共有すると例外が発生します。今回の PR は、テストアプリの開発で見つかった数件の可変定数を対象に、freeze または ActiveSupport::Ractors 系のヘルパーで不変化させる作業です。変更は動作を変更しないことが前提で、既存コードへの影響を最小限に抑える設計になっています。
技術的な変更
actionpack/lib/action_controller/metal/allow_browser.rb
**SETS** 定数はブラウザバージョンの許容リストを保持していますが、内部ハッシュがミュータブルな状態でした。変更前は modern: { safari: 17.2, chrome: 120, firefox: 121, opera: 106, ie: false } というリテラルがそのまま格納されていました。
@@ -72,7 +72,7 @@ def allow_browser(versions:, block:)
class BrowserBlocker # :nodoc:
SETS = {
- modern: { safari: 17.2, chrome: 120, firefox: 121, opera: 106, ie: false }
+ modern: { safari: 17.2, chrome: 120, firefox: 121, opera: 106, ie: false }.freeze
}.freeze
attr_reader :request, :versions
このようにハッシュ自体に .freeze を付与し、SETS 全体が完全に不変になるようにしました。結果として、Ractor で SETS を共有しても安全性が保証されます。
actionview/lib/action_view/helpers/tag_helper.rb
**PRE_CONTENT_STRINGS** はフォーム要素の前置文字列を管理するハッシュで、デフォルト値を Proc で生成していました。元の実装は Hash.new { "" } であり、内部の Proc がミュータブルでした。
@@ -41,7 +41,7 @@ module TagHelper
TAG_TYPES.merge! CLASS_PREFIXES.index_with(:class)
TAG_TYPES.freeze
- PRE_CONTENT_STRINGS = Hash.new { "" }
+ PRE_CONTENT_STRINGS = Hash.new(&ActiveSupport::Ractors.shareable_proc { "" })
PRE_CONTENT_STRINGS[:textarea] = "\n"
PRE_CONTENT_STRINGS["textarea"] = "\n"
PRE_CONTENT_STRINGS.freeze
Hash.new に ActiveSupport::Ractors.shareable_proc を渡すことで、生成される Proc が Ractor‑shareable かつ不変となります。さらに定数全体を freeze しているため、他のスレッドや Ractor が参照しても状態が保護されます。
activesupport/lib/active_support/log_subscriber.rb
**LEVEL_CHECKS** はロガーのログレベル判定をラムダで保持しており、各ラムダがミュータブルでした。変更により、ActiveSupport::Ractors.shareable_lambda を使用して不変ラムダに置き換えました。
@@ -67,9 +67,9 @@ class LogSubscriber < Subscriber
class_attribute :log_levels, instance_accessor: false, default: {} # :nodoc:
LEVEL_CHECKS = {
- debug: -> (logger) { !logger.debug? },
- info: -> (logger) { !logger.info? },
- error: -> (logger) { !logger.error? },
+ debug: ActiveSupport::Ractors.shareable_lambda(&-> (logger) { !logger.debug? }),
+ info: ActiveSupport::Ractors.shareable_lambda(&-> (logger) { !logger.info? }),
+ error: ActiveSupport::Ractors.shareable_lambda(&-> (logger) { !logger.error? }),
}.freeze
class << self
shareable_lambda により各ラムダが Ractor で安全に共有でき、LEVEL_CHECKS 全体が freeze されているため不変性が保証されます。
設計判断
今回の変更は freeze と ActiveSupport::Ractors.shareable_* 系ヘルパーを組み合わせて、定数の不変化と Ractor 共有可能性を同時に満たす方針です。freeze は最もシンプルで既存コードへの影響が最小ですが、Proc や Lambda の場合は単に freeze できないため、shareable ヘルパーが採用されました。これにより、可変オブジェクトを安全に Ractor 間で参照でき、将来的な Ractor 対応拡張が容易になります。また、定数自体を freeze したまま残すことで、意図しない再代入や変更を防止し、コードベース全体の不変性を強化しています。
まとめ
本 PR は、Rails の定数に対し freeze と ActiveSupport::Ractors.shareable_proc / shareable_lambda を導入し、Ractor で安全に共有できるようにした変更です。挙動は変えずに不変性を保証することで、フレームワーク全体の Ractor‑safe 化に寄与しています。