Inflection ルールを起動後にフリーズして Ractor 共有可能化
Rails アプリケーションが起動した後でも Inflector の変化ルールを変更できる点を解消し、全てのインスタンスをフリーズして Ractor で安全に共有できるようになりました。
背景
アプリケーション起動後に inflection ルール がミュート可能であることは、テーブル名やクラス名の自動変換が実行時に予期せぬ形で変化し、デバッグが困難になるリスクを伴います。加えて、Ractor が導入された環境では、オブジェクトが共有可能であることがスレッド間安全性の前提となりますが、ミュータブルな inflection インスタンスはその前提を満たしませんでした。この背景から、起動後の変更を防止しつつ Ractor でも利用できる設計が求められました。
技術的な変更
ActiveSupport::Inflector::Inflections クラスに freeze メソッド が追加され、インスタンス内部のルール配列と正規表現パターンをすべて凍結します。フリーズ処理は次の手順で実行されます。
def freeze
build_pattern
@members.each(&:freeze)
@members.freeze
@pattern.freeze
super
end
パターン生成は新たに導入されたプライベートメソッド build_pattern が担当し、@members から正規表現を組み立てて @pattern に設定します。
private
def build_pattern
members_pattern = Regexp.union(@members.map { |w| /#{Regexp.escape(w)}/i })
@pattern = /\b#{members_pattern}\Z/i
end
さらに、全ロケールのインスタンスを取得できるクラスメソッド all_instances が追加され、既存の instance メソッドに加えてすべてのインスタンスを列挙可能にしました。
def self.all_instances # :nodoc:
[@__en_instance__] + @__instance__.values
end
Rails::Railtie に新しい initializer active_support.freeze_inflections が導入され、config.after_initialize フック内で Inflections.all_instances.each(&:freeze) が呼び出されます。これにより、アプリケーションが完全に起動したタイミングで全インスタンスがフリーズされます。
initializer "active_support.freeze_inflections" do
config.after_initialize do
ActiveSupport::Inflector::Inflections.all_instances.each(&:freeze)
end
end
テストでは、起動後に Inflections.all_instances が Ractor 共有可能かどうかを確認し、さらにフリーズされたインスタンスに対して変更を試みると FrozenError が発生することが検証されています。
設計判断
ミュータブルな inflection ルール自体は残しつつ、フリーズ によって変更を禁止する方針が採用されました。このアプローチは既存の Inflector.inflections API をそのまま維持でき、呼び出し側のコード改修を不要にします。全インスタンスを一括でフリーズするために after_initialize フックを利用した点は、ロケールごとのインスタンス生成タイミングに依存せず確実に対象を網羅できる設計的利点があります。
まとめ
起動後の inflection ルール変更を防止し、全インスタンスをフリーズすることで Ractor 環境でも安全に共有できるようになりました。変更は Inflections クラスに限定されたメソッド追加と Railtie の初期化子追加のみで、既存コードへの影響は最小限です。