MemoryStore#cleanup がシリアライザ非依存になるバグ修正

rails/rails

MemoryStore の cleanup がデフォルト以外のシリアライザでも例外を起こさず、期限切れエントリを正しく除去できるようになりました。

背景

ActiveSupport::Cache::MemoryStore#cleanup はキャッシュサイズ超過時に古いエントリを削除するために全キーを走査し、各エントリに対して expired? を呼びます。デフォルトの DupCoder では @data に生の Entry オブジェクトが格納されるため問題はありませんが、serializer: :marshal_7_1 などの代替シリアライザを指定すると @data にはシリアライズ済みの String が格納されます。String には expired? メソッドが存在しないため、cleanupNoMethodError をスローし、以降の書き込みが失敗してキャッシュが肥大化します。

この不具合は 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 がシリアライズ形式に依存せずエントリを正しくデシリアライズして期限判定を行うようになり、非デフォルトシリアライザ使用時の例外が解消されました。シンプルなロジック統一と回帰テストの追加により、キャッシュサイズ制御の信頼性が向上し、既存アプリケーションへの影響はありません。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
9ebe17a7

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景(Context)、技術的な変更(Technical Detail)、設計判断(Design Insight 任意)、まとめ(Conclusion)の構成が揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名付きハイライトは正しく使用されていますが、PRリンクの記法が「[PR #57671](URL)」となっており、指定の「[#123](URL)」形式になっていません。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

対象はRailsエンジニアであり、専門用語や内部実装に焦点が当たっているため適切です。初心者向けの余計な説明はありません。

パラグラフ・ライティング ⚠ WARNING

トピックセンテンス・1段落1トピック・段落長

各セクションはトピックセンテンスで始まっていますが、背景や技術的変更のセクションに明示的な結論パラグラフが抜けている箇所があります。全体の可読性は問題ありませんが、セクション完結の観点で改善が必要です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックは提供されたDiffと完全に一致しており、変更箇所・追加行ともに正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語(serializer, DupCoder, Entry, expired? など)はPRと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

記事の技術的説明はPRの問題点・修正内容・テスト追加の記述と合致しており、因果関係も正しく示されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

すべての主張はPRのタイトル、説明、Diffに裏付けられており、根拠のない推測や外部知識は含まれていません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #57671、ファイルパス、行数などの数値は正確です。誤った数値は見られません。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRの内容(MemoryStore#cleanup の NoMethodError 修正)を適切に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事はPRに記載された情報以外の外部知識(LTS、リリース日程等)を含んでいません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PRの記述と一致しています。