Active RecordのRactor対応:定数のfreeze化とキャッシュの共有化
Active Recordの各モジュールで定数やキャッシュオブジェクトを積極的にfreezeすることで、Ractorコンテキストでの安全な共有を可能にする変更が導入されました。#57323に続くRactor対応の一環です。
背景
RactorはRubyの並行実行機構であり、Ractor間でオブジェクトを共有するには、そのオブジェクトが shareable(共有可能)である必要があります。Rubyのshareableオブジェクトの条件のひとつが「凍結(freeze)されていること」です。Active Recordのモジュールがクラス・定数レベルで可変オブジェクトを保持している場合、そのモジュールはRactorをまたいで安全に利用できません。
前回の#57323ではStyle/MutableConstant Copを有効化し、リテラルの定数定義に対してfreezeを強制する仕組みが整備されました。今回のPR #57475はその続きとして、Active Record内の具体的なモジュールに対してfreezeと定数化を適用しています。
技術的な変更
このPRでは4ファイルにわたり、可変オブジェクトのfreezeと、メモ化パターンから定数参照への切り替えが行われています。変更はいずれも「Ractor間で共有されうるオブジェクトを不変にする」という一貫した方針に従っています。
ReadonlyAttributesのデフォルト値のfreeze(activerecord/lib/active_record/readonly_attributes.rb)では、_attr_readonlyのデフォルト値として渡していた[]を[].freezeに変更しました。
変更前:
class_attribute :_attr_readonly, instance_accessor: false, default: []
変更後:
class_attribute :_attr_readonly, instance_accessor: false, default: [].freeze
WhereClauseの空インスタンスの定数化(activerecord/lib/active_record/relation/where_clause.rb)では、@empty ||= new([]).freezeというメモ化パターンを廃止し、EMPTY定数として定義し直しました。
変更前:
def self.empty
@empty ||= new([]).freeze
end
変更後:
EMPTY = new([]).freeze
def self.empty
EMPTY
end
Typeモジュールのデフォルト型の定数化(activerecord/lib/active_record/type.rb)では、@default_value ||= Value.newというメモ化を廃止し、DEFAULT_TYPE定数として定義し直しました。
変更前:
def default_value
@default_value ||= Value.new
end
変更後:
DEFAULT_TYPE = Value.new.freeze
def default_value
DEFAULT_TYPE
end
delegation.rbのキャッシュのfreeze(activerecord/lib/active_record/relation/delegation.rb)では、initialize_relation_delegate_cacheメソッドの末尾で@relation_delegate_cache.freezeを呼び出し、構築後のキャッシュHashを不変化します。これにより、キャッシュが初期化後に変更されないことをRactorの仕組みとして保証します。
設計判断
メモ化パターン(||=)から定数への置き換えが、Ractor対応の主要な設計戦略として採用されています。
インスタンス変数を用いたメモ化(@foo ||= SomeObject.new)は、初回アクセス時にオブジェクトを生成してインスタンス変数に格納する慣用的なパターンです。しかしこの方式では、オブジェクトがRactor間で共有される前にfreezeされる保証がなく、shareableの条件を満たせません。定数として宣言し起動時に即座にfreezeすることで、ロード時点でshareableな状態が確定します。
また、ARRAY_WITH_EMPTY_STRING = [""}.freezeのような既存の定数定義もwhere_clause.rb内に存在しており、今回のEMPTY定数の追加はその一貫した書き方に沿っています。変更後のメソッドself.emptyは定数への委譲のみになり、インターフェースは変わらずに内部実装だけがRactor対応に切り替わっています。
まとめ
このPRはメモ化パターンや可変デフォルト値を定数・freeze済みオブジェクトに置き換えることで、Active RecordをRactorの共有可能オブジェクトの要件に近づけています。インターフェースを変えずに内部実装のみを不変化する手法は、既存アプリケーションへの影響を最小化しながらRactor対応を段階的に進める上で有効なアプローチです。