Don't mutate the names array passed to `Cache::Store#delete_multi`

rails/rails

ActiveSupport::Cache::Store#delete_multi が呼び出し元の配列を破壊的に変更していた問題を解消し、同系統メソッドとの振る舞いを統一しました。

背景

Cache::Store#delete_multi は名前空間付きキーに正規化する過程で names.map! を用いて配列をインプレースで書き換えていました。このため、delete_multi を呼び出した直後に names の内容が "ns:foo" のように変化し、同じ配列を再利用すると二重名前空間化や FrozenError が発生していました。

同じくマルチキー操作を提供する read_multi, write_multi, fetch_multi はすべて新しいコレクションを生成しており、配列を破壊しない実装となっていました。結果として delete_multi だけが例外的な振る舞いを示し、開発者に予期しない副作用をもたらしていました。

この不整合はテストでも再現でき、配列が変更されることで二度目の削除が失敗し、キャッシュに残存データが残ることが確認されていました。

技術的な変更

delete_multi の実装を names.map! から非破壊的な names.map に置き換え、正規化後の配列をローカル変数に再代入するだけにしました。これにより呼び出し元の配列は変更されません。

@@ -702,7 +702,7 @@ def delete_multi(names, options = nil)
         return 0 if names.empty?

         options = merged_options(options)
-        names.map! { |key| normalize_key(key, options) }
+        names = names.map { |key| normalize_key(key, options) }

         instrument_multi(:delete_multi, names, options) do
           delete_multi_entries(names, **options)

さらに、activesupport/test/cache/behaviors/cache_store_behavior.rbdelete_multi が配列を変化させないことを保証するテスト を追加しました。テストは凍結配列でも正しく削除でき、呼び出し後に元のキー配列が保持されていることを明示しています。

@@
   def test_delete_multi_does_not_mutate_names
     key = SecureRandom.alphanumeric
     other_key = SecureRandom.alphanumeric
     names = [key, other_key].freeze

     @cache.write(key, "bar")
     @cache.write(other_key, "world")
     assert_equal 2, @cache.delete_multi(names)
     assert_equal [key, other_key], names

     @cache.write(key, "bar")
     @cache.write(other_key, "world")
     assert_equal 2, @cache.delete_multi(names)
     assert_not @cache.exist?(key)
     assert_not @cache.exist?(other_key)
   end

設計判断

delete_multi の実装を 破壊的な map! から非破壊的な map に変更した根拠は、一貫性副作用回避 です。周辺の read_multiwrite_multifetch_multi がすべて新しいコレクションを作成していることから、同様の振る舞いを期待するのは自然です。また、下流の delete_multi_entriesinstrument_multi は正規化済み配列を 読み取り専用 で扱うだけで、配列のミューテーションに依存していません。したがって、実装を変更しても既存ロジックへの影響はありません。

この変更により、呼び出し側が配列を再利用できるだけでなく、凍結配列を渡した際に例外が発生しなくなるため、API の堅牢性が向上します。設計上のトレードオフは無く、コード量の増加も map への置き換えとテスト追加のみという最小限の改修で済んでいます。

まとめ

delete_multi が配列を破壊しないようにしたことで、Rails のキャッシュ API が全マルチキー操作で同一の非破壊的振る舞いを示すようになり、予期しない副作用や凍結配列でのエラーが解消されました。テスト追加により変更が検証され、今後の拡張でも安全に利用できる基盤が整いました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
04c205c2

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文があり、背景・技術的変更・設計判断・まとめの各セクションが順に配置されており、総論→各論→結論の構成が明確です。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは ```ruby:ファイルパス の形式で正しく記述され、PRリンクは [#57647](URL) という正しいGitHubリンク記法が使用されています。

対象読者への適合性 ✓ PASS

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

内容はRailsのキャッシュ実装に詳しいエンジニア向けで、初心者向けの過度な説明はありません。

パラグラフ・ライティング ✓ PASS

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

各セクションは冒頭のトピックセンテンスで要点を示し、1段落に1トピック、6文以内で構成されています。段落間は空行で区切られています。

Diff内容との照合 ✓ PASS

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

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

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用された技術用語(map!、map、normalize_key、instrument_multi など)はPRで用いられている語と合致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

変更理由・影響・設計判断がPRの記述と一致し、技術的に正確です。

事実の突合 ✓ PASS

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

記事の全ての主張はPRタイトル、説明、Diffで裏付けられており、推測や捏造はありません。

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

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

PR番号 #57647 だけが記載されており、正確です。その他数値的記述はありません。

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

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

記事タイトルがPRタイトルと完全に一致しています。

外部知識の正確性 ✓ PASS

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

バージョンサポートやリリース日程等、PR外の外部知識は一切含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の記述はなく、PR情報と食い違う表現もありません。