`class_attribute`をRactor対応に再設計、クロージャからivarへ
ActiveSupport::ClassAttributeの実装が、クロージャベースからインスタンス変数ベースへと再設計され、Ractor安全な動作を実現しました。パフォーマンスへの影響は10%未満に抑えられています。
背景
従来のclass_attributeは、値をクロージャに閉じ込めて管理するアプローチを採っており、これがRactorとの根本的な非互換の原因でした。Ractorはオブジェクトの共有に厳しい制約を課すため、クロージャが参照するオブジェクトがshareable(共有可能)でない限り、複数のRactorから安全にアクセスできませんでした。
また、このPRはバグの修正も含んでいます。シングルトンクラスへの代入時に、instance_reader: falseが指定されていてもinstance_readerが無条件に定義されてしまう問題がありました。新しいテストケース「disabling instance reader is not bypassed by assigning on the singleton class」がその修正を保証します。
技術的な変更
実装の核心は、「値を保持するメソッド」を「オーナーを返すメソッド」と「値を読むattr_reader」に分割したことです。
変更前は、__class_attr__nameという1つのプライベートメソッドがクロージャ内の変数として値を保持していました。値が変更されるたびにdefine_methodでメソッド自体を再定義し、新しいクロージャに値を閉じ込めていました。
変更後は、責務を2つに分離しています:
-
__class_attr_#{name}_owner: 値の「オーナー」となるクラスを返すメソッド。クラス自体はRactor間で共有可能なため、Ractor.shareable_procでwrapできる -
__class_attr_#{name}:attr_readerとして定義され、オーナークラスのインスタンス変数@__class_attr_#{name}から値を読む
# 変更後のメソッド定義
reader_method = :"__class_attr_#{name}"
owner_method = :"__class_attr_#{name}_owner"
singleton_class.attr_reader(reader_method)
class_methods << "def #{name}; #{owner_method}.#{reader_method}; end"
class_methods << <<~RUBY
def #{name}=(value)
if #{owner_method}.equal?(self)
@#{reader_method} = value
else
::ActiveSupport::ClassAttribute.redefine(self, :#{name}, :#{owner_method}, :#{reader_method}, value, #{!!instance_reader})
end
end
RUBY
redefineメソッド内では、値をインスタンス変数にセットし、オーナーを示すprocをRactor.shareable_procでwrapします。
def redefine(owner, name, owner_method, reader_method, value, instance_reader)
ivar_name = :"@#{reader_method}"
owner.instance_variable_set(ivar_name, value)
owner_proc =
if defined?(Ractor.shareable_proc)
Ractor.shareable_proc { owner }
else
-> { owner }
end
redefine_method(owner.singleton_class, owner_method, private: true, &owner_proc)
end
読み取り時のフローはクラスメソッド → owner_method → attr_readerとなります。Base.settingを呼ぶとBase.__class_attr_setting_owner(= Base)を取得し、そのクラスの__class_attr_setting attr_readerでインスタンス変数を読みます。継承したサブクラスが値を上書きしていない場合はowner_methodが親クラスを返し、親クラスのivarが参照されます。
ActiveSupport::Callbacksの最適化ハックも対応して更新されています。set_callbacksが値の変更を検出する際にチェックするメソッド名が、__class_attr__callbacksから__class_attr__callbacks_ownerに変更されました。
設計判断
「オーナークラスのivar」を値の保管場所とする設計が、Ractor安全性の鍵です。Ractorはivarへのアクセスに対して明確なセマンティクスを持ちます。値がshareable(例:freeze済みオブジェクト)であれば全Ractorから読め、そうでなければメインRactorのみが読めるという自然な制約が適用されます。これは再定義で回避するよりも堅牢な境界です。
Ractor.shareable_procが利用可能かどうかで分岐している点も見逃せません。この分岐により、Ractorをサポートしないルビーバージョンとの後方互換性を維持しつつ、サポートする環境では最適化されたprocを使う設計になっています。
シングルトンクラスへの代入時のinstance_reader修正については、owner.singleton_class? && !owner.attached_object.is_a?(Module) && instance_readerという条件で、インスタンスがModuleでない(つまり通常オブジェクトのシングルトンクラスである)場合にのみself.singleton_class.reader_methodを参照するインスタンスメソッドを再定義する処理が追加されました。これにより従来のバグが解消されています。
まとめ
この変更は、クロージャによる値の閉じ込めという従来の実装を、クラスのivarを権威ある保管場所とするアーキテクチャへと転換しました。Ractor安全性をパフォーマンスをほぼ維持したまま獲得しており、RubyのRactor普及に向けたActive Supportの対応を前進させる変更です。