MemoryStore#cleanup がシリアライザ非依存になるバグ修正
MemoryStore の cleanup がデフォルト以外のシリアライザでも例外を起こさず、期限切れエントリを正しく除去できるようになりました。
背景
ActiveSupport::Cache::MemoryStore#cleanup はキャッシュサイズ超過時に古いエントリを削除するために全キーを走査し、各エントリに対して expired? を呼びます。デフォルトの DupCoder では @data に生の Entry オブジェクトが格納されるため問題はありませんが、serializer: :marshal_7_1 などの代替シリアライザを指定すると @data にはシリアライズ済みの String が格納されます。String には expired? メソッドが存在しないため、cleanup が NoMethodError をスローし、以降の書き込みが失敗してキャッシュが肥大化します。
この不具合は read_entry が既に deserialize_entry を呼び出して正しくエントリを復元しているのに対し、cleanup が生データを直接参照していた点に起因します。結果として、非デフォルトシリアライザを利用するすべてのアプリケーションでキャッシュの自動削除が機能しなくなります。
技術的な変更
cleanup 内部でエントリ取得を entry = @data[key] から entry = deserialize_entry(@data[key]) に変更しました。これにより、シリアライズされた文字列が自動的に Entry オブジェクトへ復元され、expired? メソッドが安全に呼び出せます。変更前後の差分は次の通りです。
@@
- entry = @data[key]
+ entry = deserialize_entry(@data[key])
deserialize_entry はキーが存在しない場合に nil を返すガードロジックを持つため、既存の if entry && entry.expired? 条件式はそのまま機能し続けます。コード増減は 1 行のみで、動作に影響を与えないシンプルな修正です。
さらに、非デフォルトシリアライザでの挙動を検証するテストが追加されました。テストは serializer: :marshal_7_1 を設定したストアに対し、期限切れエントリと有効エントリを書き込み、時間を進めて cleanup を呼び出すことで例外が発生しないこと、期限切れエントリが除去され有効エントリが残ることを確認します。
@@
- def test_cleanup_with_non_dup_coder_serializer
- @cache = lookup_store(serializer: :marshal_7_1)
- @cache.write("expired", "x" * 100, expires_in: 0.01)
- @cache.write("fresh", "y" * 100)
-
- Time.stub(:now, Time.now + 1.minute) do
- @cache.cleanup
-
- assert_nil @cache.read("expired")
- assert_equal "y" * 100, @cache.read("fresh")
- end
- end
+ def test_cleanup_with_non_dup_coder_serializer
+ @cache = lookup_store(serializer: :marshal_7_1)
+ @cache.write("expired", "x" * 100, expires_in: 0.01)
+ @cache.write("fresh", "y" * 100)
+
+ Time.stub(:now, Time.now + 1.minute) do
+ @cache.cleanup
+
+ assert_nil @cache.read("expired")
+ assert_equal "y" * 100, @cache.read("fresh")
+ end
+ end
設計判断
この修正は cleanup の実装を read_entry と同様のデシリアライズ手順に揃える という最小限の変更です。既存のインターフェースやオプション設定を変更せず、既存コードへの侵入度を抑えることで後方互換性を確保しました。代替シリアライザをサポートするために新たな API を導入せず、内部ロジックだけで問題を解決した点が設計上の重要な判断です。
また、テスト追加により シリアライザの多様化に対する回帰防止策 が組み込まれました。これにより、将来的に別のコーダーやカスタムシリアライザが導入された場合でも cleanup が正しく機能することを CI が保証します。設計上のトレードオフはなく、コード量増加は 1 行、テスト追加は 13 行という軽微なコストで堅牢性が向上しました。
まとめ
MemoryStore#cleanup がシリアライズ形式に依存せずエントリを正しくデシリアライズして期限判定を行うようになり、非デフォルトシリアライザ使用時の例外が解消されました。シンプルなロジック統一と回帰テストの追加により、キャッシュサイズ制御の信頼性が向上し、既存アプリケーションへの影響はありません。