`Rails.cache.read` に `delete: true` オプションを追加——Redisのアトミックな読み取り削除に対応

rails/rails

Rails.cache.readdelete: true オプションが追加され、RedisCacheStore においてRedisの GETDEL コマンドを使ったアトミックな読み取りと削除が可能になりました。OTPコードやワンタイムトークンなど、一度しか消費できない値の管理で発生しがちな競合状態を根本から排除できます。

背景

これまで「読み取ってから削除する」パターンを実装するには、readdelete を別々に呼び出す必要がありました。この2ステップ操作には本質的な競合状態が存在し、複数のプロセスが同じキーを同時に読み取った場合、いずれも値を取得できてしまいます。OTPコードや使い切りトークンのような「一度しか消費できない」ユースケースでは、この競合が致命的なセキュリティ上の問題になり得ます。

Redis 6.2で導入された GETDEL コマンドは、値の取得とキーの削除をサーバー側で単一のラウンドトリップにまとめ、この問題をアトミックに解決します。本PRはそのコマンドをRailsのキャッシュAPIから直接利用できるようにしています。

技術的な変更

変更は RedisCacheStoreLocalCache の2層のキャッシュ実装と、テストの3箇所にわたっています。

RedisCacheStore#read_serialized_entry の拡張

RedisCacheStore の内部メソッドである read_serialized_entrydelete キーワード引数を受け取るよう拡張されました。options[:delete] が真のとき、c.get(key) の代わりに c.getdel(key) を呼び出します。

変更前:

def read_serialized_entry(key, raw: false, **options)
  failsafe :read_entry do
    redis.then { |c| c.get(key) }
  end
end

変更後:

def read_serialized_entry(key, raw: false, **options)
  failsafe :read_entry do
    redis.then { |c| options[:delete] ? c.getdel(key) : c.get(key) }
  end
end

変更は1行の三項演算子のみであり、既存の read 呼び出しへの影響はありません。

LocalCache でのバイパスと整合性維持

Strategy::LocalCache#read_serialized_entry では、delete: true のとき専用の分岐が設けられています。ローカルキャッシュが有効な場合でも super(Redisへのアクセス)を直接呼び出し、その後 cache.delete_entry(key) でインメモリキャッシュの該当エントリを削除します。

変更前:

def read_serialized_entry(key, raw: false, **options)
  if cache = local_cache
    hit = true
    entry = cache.fetch_entry(key) do
      hit = false
      super
    end
    options[:event][:store] = cache.class.name if hit && options[:event]
    entry
  else
    super
  end
end

変更後:

def read_serialized_entry(key, raw: false, **options)
  if cache = local_cache
    if options[:delete]
      entry = super
      cache.delete_entry(key)
      entry
    else
      hit = true
      entry = cache.fetch_entry(key) do
        hit = false
        super
      end
      options[:event][:store] = cache.class.name if hit && options[:event]
      entry
    end
  else
    super
  end
end

この実装には2つの重要な意図があります。まず、ローカルキャッシュに古い値が残っていても GETDEL は必ずRedisまで到達する必要があるため、fetch_entry(ローカルキャッシュを先に参照するパス)をバイパスしています。次に、Redisの削除操作が完了した後にローカルキャッシュも削除することで、両レイヤー間の整合性を維持しています。

他のキャッシュストアへの影響

delete: true オプションは **options のスプラット経由で他のキャッシュストアに渡されますが、それらのストアはこのオプションを認識しないため、静かに無視されます。これは既存のキャッシュストアとの後方互換性を保ちながら機能を追加する、Railsらしいアプローチです。

設計判断

Redisレイヤーでのアトミック操作をRailsのキャッシュ抽象化に薄く包んだ設計が採用されました。

新しい専用メソッド(例: read_and_delete)を追加するのではなく、既存の read にオプションを追加することで、APIの増加を最小限に抑えています。PR説明にもあるとおり、このオプションはRedis固有であり、他のキャッシュストアでは暗黙的に無視される動作が明確に文書化されています。これは汎用的な抽象化よりも、特定のバックエンドの能力を直接活用することを優先した判断といえます。

なお、PR内では GETDEL が導入されたRedis 6.2未満のバージョンへの対応について懸念も示されています。現時点ではバージョンチェックは実装されておらず、古いバージョンのRedisを使用している場合はエラーが発生する可能性があります。

まとめ

本PRは、キャッシュAPIの既存インターフェースを最小限の変更で拡張し、Redisの GETDEL コマンドが持つアトミック性を直接活用できるようにしました。読み取りと削除の間に競合状態が生じ得るワンタイムトークン管理のような実装パターンを、Rails.cache.read(key, delete: true) という一行で置き換えられる点は、実用上の価値が高い変更です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
8d60627e

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という3部構成が明確に適用されており、非常に分かりやすい構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```言語:ファイルパス)、PR番号のリンク記法([PR #...](URL))ともに正しく使用されています。

対象読者への適合性 ✓ PASS

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

「RedisCacheStore」「GETDEL」「競合状態」といった用語を前提としており、専門知識を持つエンジニアという対象読者に適合した内容です。

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

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

各セクションが総論→各論の構成になっており、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が遵守されています。可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロック(変更前・変更後)は、提供されたDiff情報と完全に一致しています。ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「GETDEL」「アトミック」「競合状態」「LocalCache」などの技術用語は、PR情報と一致しており、文脈上も正確に使用されています。

説明の技術的正確性 ✓ PASS

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

「LocalCacheをバイパスする」意図や、「他のキャッシュストアでは無視される」挙動など、DiffとPR Descriptionに基づいた技術的な説明が正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionやDiffから裏付けられており、ハルシネーション(捏造)は検出されませんでした。「Redis 6.2で導入」という情報もPR内に記載があります。

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

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

PR番号(#56807)やバージョン番号(Redis 6.2)などの数値・固有名詞はすべて正確に記載されています。

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

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

記事のタイトルはPRのタイトル(add support to new option delete: true)の内容を正確に反映しつつ、その技術的意義(Redisのアトミックな読み取り削除に対応)を補足しており、秀逸です。

外部知識の正確性 ✓ PASS

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

記事に含まれる情報はすべてPR情報(Title, Description, Diff)に基づいており、PRに記載のない外部知識(LTS情報、リリース日程など)の追加はありません。

時間表現の正確性 ✓ PASS

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

「これまで」「導入された」といった時間表現は、PRの文脈と一致しており、事実を歪曲するような記述はありません。