MessagePack が Time カラムを持つレコードのシリアライズで失敗するバグを修正
Rails 7.1 で追加された ActiveRecord::MessagePack が、time カラムを持つレコードをキャッシュに書き込む際に NoMethodError を投げていた問題を解消します。ActiveSupport::MessagePack::CacheSerializer に組み込まれた拡張が、時間型の内部表現とマッチせずに例外が発生していた点が修正の核心です。
背景
ActiveRecord::MessagePack はモデルオブジェクトを MessagePack 形式に変換し、Rails.cache.write などのキャッシュ層で利用できるようにします。その実装は ActiveRecord::MessagePack::Extensions を ActiveSupport::MessagePack::CacheSerializer にバインドし、MessagePack の型ファクトリに対して Rails の各属性型を登録する仕組みです。
time カラムは ActiveRecord::Type::Time::Value という DelegateClass(::Time) のサブクラスでラップされます。MessagePack ファクトリは ::Time クラスに対して型 ID を登録しているため、デリゲートサブクラスはクラス一致でマッチせず、to_msgpack が未定義というエラーが発生しました。結果として Rails.cache.write が例外で中断し、Solid Cache 等のキャッシュ機構が利用できなくなっていました。
この不具合は MessagePack サポートが Rails 本体に取り込まれた 2023 年以降、time カラムを持つモデルがキャッシュ対象になったときに初めて顕在化していました。従来のテストは time カラムを含まないモデルだけを対象としていたため、問題は潜在的なままでした。
技術的な変更
ActiveRecord::MessagePack::Extensions に ActiveRecord::Type::Time::Value 用の型登録を追加し、型 ID 118 として MessagePack ファクトリに登録します。これによりデリゲートクラスも再帰的にシリアライズ対象となります。
registry.register_type 118, ActiveRecord::Type::Time::Value,
packer: method(:write_time_value),
unpacker: method(:read_time_value),
recursive: true
write_time_value と read_time_value の2メソッドを実装し、デリゲートオブジェクトから内部の Time インスタンスを取得してシリアライズし、逆にデシリアライズ時に新たな ActiveRecord::Type::Time::Value を生成します。
def write_time_value(value, packer)
packer.write(value.__getobj__)
end
def read_time_value(unpacker)
ActiveRecord::Type::Time::Value.new(unpacker.read)
end
テストは activerecord/test/cases/message_pack_test.rb に test_roundtrips_time_attribute を追加し、Topic モデルの bonus_time(time 型)を含むレコードが正しく往復できることを検証しました。また、型 ID マッピングを確認する test_enshrines_type_IDs にも ID 118 のエントリを追加しています。
test "roundtrips time attribute" do
topic = Topic.new(bonus_time: "09:30:00")
assert_equal topic.attributes, roundtrip(topic).attributes
end
これらの変更は既存の ::Time 用登録 (type ID 119 など) に影響を与えず、ActiveRecord::MessagePack を利用する全てのアプリケーションで time カラムを安全にキャッシュできるようにします。
設計判断
MessagePack の型ファクトリに ActiveRecord::Type::Time::Value を直接登録する方針が採用されました。代替案としては ActiveRecord::MessagePack の内部ロジックでデリゲートを展開してから既存の ::Time 登録を利用する方法や、別名キー time_value を導入する方法が議論されましたが、既存の登録方式と同一のインターフェースを保ちつつ後方互換性を最大化できる点で直接登録が最適と判断されました。
この設計は 再帰的(recursive) なシリアライズを明示的に有効化することで、他のラップ型(例: Binary::Data)と同様の取り扱いを実現しています。結果として、Rails の設定やモデル定義を変更せずに MessagePack キャッシュの信頼性が向上し、開発者は既存の API をそのまま利用できます。