`LookupContext` の定数ミュータビリティをクラスivarへ移行してRactor対応を前進させる
LookupContext::Accessors::DEFAULT_PROCS をフリーズ不可能な定数から管理可能なクラスインスタンス変数へ移行し、Ractor対応に向けたイミュータブル定数整備の取り組みを継続しました。
背景
RailsをRactorで利用するためには、Ractor間で共有されるクラス・モジュールの定数がすべてShareableである必要があります。#57323 で Style/MutableConstant Copを有効化し、リテラル定数を広くフリーズする対応が進められましたが、DEFAULT_PROCS はその時点で rubocop:disable コメントで除外されていました。DEFAULT_PROCS は register_detail が呼ばれるたびに新しいエントリが追加されるミュータブルなHashであり、単純にフリーズすることができなかったためです。本PRはその制約を設計で解決する後続の変更です。
技術的な変更
DEFAULT_PROCS 定数を廃止し、同等の情報をフリーズされたクラスインスタンス変数 default_procs として管理するよう変更されました。新しいエントリを追加する際は、既存のHashをコピー・マージして新たなフリーズ済みHashを再代入する方式を採用しています。
変更前:
singleton_class.attr_accessor :registered_details
self.registered_details = []
def self.register_detail(name, &block)
registered_details << name
Accessors::DEFAULT_PROCS[name] = block
# ...
end
module Accessors
DEFAULT_PROCS = {} # rubocop:disable Style/MutableConstant
end
変更後:
singleton_class.attr_accessor :default_procs
self.default_procs = {}.freeze
def self.registered_details
self.default_procs.keys
end
def self.register_detail(name, &block)
self.default_procs = self.default_procs.merge(name => block).freeze
# ...
end
登録済み詳細の一覧として使われていた配列 registered_details も、default_procs のキーから導出するよう変更されています。これにより、default_procs が「プロシージャの格納場所」と「登録済み詳細の一覧」という2つの役割を1つのデータ構造で担うことになり、クラスインスタンス変数が1つ削減されました。
DEFAULT_PROCS を参照していた内部メソッドも、LookupContext.default_procs を参照するよう更新されています。
# detail_args_for_any 内
- details[k] = Accessors::DEFAULT_PROCS[k].call
+ details[k] = LookupContext.default_procs[k].call
# initialize_details 内
- target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call
+ target[k] = details[k] || LookupContext.default_procs[k].call
設計判断
「更新のたびにフリーズ済みの新Hashへ置き換える」パターン が採用されました。
register_detail はアプリケーション起動時に少数回だけ呼ばれる操作であり、その都度 merge で新たなHashを生成してもコストは無視できます。一方で、生成されたHashは以後読み取り専用として扱えるため、Ractor間で安全に共有できる構造になります。ミュータブルな定数を rubocop:disable で黙認し続けるのではなく、データ構造のライフサイクル(起動時の書き込み・実行時の読み取り)に合わせてイミュータブル性を保証した判断といえます。
また、Accessors::DEFAULT_PROCS という名前空間をまたいだ定数参照をやめ、LookupContext.default_procs というクラスメソッドに一本化したことで、アクセスパスが明確になっています。
まとめ
ミュータブルな定数を「コピーしてフリーズ・再代入する」クラスivarへ置き換えることで、Ractor対応の制約を満たしながら内部の状態管理を簡素化しています。起動時のみ変更されるデータを実行時にフリーズ済みとして扱うこのパターンは、Ractor対応を進めるRailsの他の箇所にも応用されていく可能性があります。