InheritableOptions を Ractor セーフにするための凍結ロジック変更
InheritableOptions が Ractor 環境で安全に共有できるよう、freeze 時のデフォルトプロック削除とキー追跡ロジックを導入しました。これにより、継承ハッシュの参照が原因で生じていた共有不可問題が解消されます。
背景
Ractor はオブジェクトの共有を明示的に許可しなければならないため、InheritableOptions が内部で保持している default_proc が参照している親ハッシュが共有不可になると、オブジェクト全体を Ractor で安全に扱えませんでした。現在の実装では default_proc がキー不在時に親ハッシュを参照するため、プロック自体が親オブジェクトへの参照を持ち、Ractor.make_shareable が適用できませんでした。そこで、プロックと親オブジェクトの相互依存を解消しつつ、継承機能を維持する手段が求められました。
技術的な変更
activesupport/lib/active_support/ordered_options.rb に freeze メソッドが新たに実装され、凍結時にデフォルトプロックを除去し、親ハッシュを凍結した上で自身のハッシュをマージします。これにより、default_proc が不要になり、オブジェクト全体が shareable になる前提が整います。
def freeze
return self if frozen?
@own_keys = own_keys.dup.freeze
self.default_proc = nil
@parent.freeze
replace(to_h)
super
end
キー集合の取得ロジックも改良され、keys が親ハッシュと自身のキーの和集合を返すようになりました。内部的に元々の own_key? をプライベートメソッド _own_keys にリネームし、own_keys は凍結前に保存した @own_keys か、元のキー集合を返すように統一しました。
alias_method :_own_keys, :keys
private :_own_keys
def keys
@parent.keys | super
end
private
def own_key?(key)
own_keys.include?(key.to_sym)
end
def own_keys
@own_keys || _own_keys
end
overridden? メソッドはそのまま残しつつ、own_key? の実装が新しいキー追跡に依存するようになり、凍結後でも正しくオーバーライド判定が行えるようになりました。
テストコード activesupport/test/ordered_options_test.rb では、ActiveSupport::Testing::RactorsAssertions を取り入れ、assert_ractor_make_shareable による Ractor 共有可能性の検証が追加されました。また、keys の順序保証と凍結後の overridden? 動作をそれぞれ確認するテストが新設されています。
require "active_support/testing/ractors_assertions"
class OrderedOptionsTest < ActiveSupport::TestCase
include ActiveSupport::Testing::RactorsAssertions
def test_inheritable_options_ractor_shareable
object = ActiveSupport::InheritableOptions.new(one: "first value")
object[:two] = "second value"
object["three"] = "third value"
assert_ractor_make_shareable(object)
end
def test_overridden_works_when_frozen
object = ActiveSupport::InheritableOptions.new(one: "first value", two: "second value")
object[:two] = "second value"
object["three"] = "third value"
object.freeze
assert object.overridden?(:two)
end
end
設計判断
デフォルトプロックを凍結時に除去し、親ハッシュをマージして置換する という方針が採用されました。プロック自体を Ractor‑shareable に変換しようとすると、親オブジェクトの安全性を保証できないという制約があり、逆にプロックを残すとオブジェクト全体が共有不可になるため、機能的にも安全面でもトレードオフが明確です。
マージ後のハッシュは従来と同様にキー参照が可能ですが、遅延参照 が失われる点は設計上の妥協です。代わりに、凍結済みオブジェクトが即座に全鍵を持つ形になるため、参照コストは減少し、Ractor 環境での使用がシンプルになります。
キー追跡用に @own_keys を導入したことで、overridden? が凍結前後で一貫した結果を返すようになり、既存の API 互換性を保ちつつ新しい安全性を提供できます。この変更は最小限のコード追加で済み、既存の InheritableOptions の振る舞いに影響を与えません。
まとめ
freeze 時にデフォルトプロックを除去し、親ハッシュをマージしてキー集合を固定化することで、InheritableOptions が Ractor 環境でも安全に共有できるようになりました。追加されたキー追跡ロジックにより overridden? の正確さが保たれ、既存 API との互換性を維持しつつ新たなマルチスレッド安全性が提供されます。