RedisCacheStoreのフェイルセーフでRedisClient::Errorを捕捉

rails/rails

ActiveSupport::Cache::RedisCacheStore は Redis が利用不可でも例外を上げずにフォールトトレラントとして動作することがドキュメントで保証されています。本 PR では、特定の Sentinel 構成下で発生する RedisClient::Error 系列が既存のフェイルセーフで捕捉されず、アプリケーションエラーに転嫁されていた問題を修正し、例外安全性を再度保証します。

背景

RedisCacheStore は内部で failsafe ラッパーを通して Redis::BaseErrorConnectionPool::Error 系列を捕捉し、エラー時はデフォルト値を返す設計です。しかし、Redis.new(sentinels: …) のようにクライアント実装が ::RedisClient に固定された場合、Redis::Client#translate_error! が呼び出されず、RedisClient::Error 系列がそのままスローされていました。この挙動は「Redis サーバーが一時的に利用できなくても例外は出ない」という公式の契約と矛盾します。

この不整合は以前の ConnectionPool::TimeoutError がフェイルセーフに漏れた問題(#54432)と同様のパターンで、#54440・#54460 で救済された経緯があります。今回の PR は同じ根本原因―例外種別の網羅的な捕捉漏れ―に対し、対象リストを拡張する形でアプローチしています。

結果として、Sentinel 環境でも Redis が一時的に落ちても Rails.cache 呼び出しは例外を発生させず、期待通りのフェイルオーバー挙動を再現します。

技術的な変更

RedisCacheStore#failsafe の例外リストをハードコーディングから FAILSAFE_ERRORS 定数へ抽象化し、::RedisClient::Error を条件付きで追加しました。defined?(::RedisClient::Error) によるガードにより、redis‑rb 4 系で RedisClient が存在しない場合でもコードがコンパイルエラーにならないよう配慮しています。

変更前:

def failsafe(method, returning: nil)
  yield
rescue ::Redis::BaseError, ConnectionPool::Error, ConnectionPool::TimeoutError => error
  @error_handler&.call(method: method, exception: error, returning: returning)
  returning
end

変更後:

FAILSAFE_ERRORS = [
  ::Redis::BaseError,
  ConnectionPool::Error,
  ConnectionPool::TimeoutError,
  (::RedisClient::Error if defined?(::RedisClient::Error)),
].compact.freeze
private_constant :FAILSAFE_ERRORS

def failsafe(method, returning: nil)
  yield
rescue *FAILSAFE_ERRORS => error
  @error_handler&.call(method: method, exception: error, returning: returning)
  returning
end

この差分に加えて、テストスイートに FailureSafetyFromRedisClientErrorTest を導入し、RedisClient::Error がスローされた際に全ての公開キャッシュメソッドがフェイルセーフのデフォルトを返すことを検証しています。テスト用ダブル RedisClientErrorRedisClientensure_connectedRedisClient::Error を発生させ、translate_error! を No‑op にオーバーライドして Sentinel パスを忠実に再現します。

結果的に、新しい例外が捕捉対象に加わり、既存の動作に影響を与えることなくフォールトトレラント機構が強化されました。

設計判断

例外リストの拡張を 定数ベース で実装する選択は、将来的な例外種別追加に対する拡張性を高めます。defined? ガードを用いることで、redis‑rb のメジャーバージョン間の API 差異をコードレベルで吸収し、後方互換性 を維持しつつ新しい例外を安全にハンドリングできます。

代替案としては、failsafe 内で個別に rescue ::RedisClient::Error を追加するや、設定フラグで例外種別を切り替える方法が考えられましたが、定数集合に集約することで例外ハンドリングロジックが一元化され、可読性と保守性が向上します。また、テスト側でクライアント実装を差し替える手法を採用した点は、実装依存を最小化しながら実環境に近いシナリオを再現できる実務的なデザインです。

総合的に、この設計は「例外捕捉の網羅性を保ちつつ、ライブラリのバージョン多様性に耐える」ことを最優先した結果と言えます。

まとめ

今回の PR は RedisCacheStore のフェイルセーフに RedisClient::Error を加えることで、Sentinel 構成下でも例外が漏れずにフォールトトレラントが保証されるようにしました。定数化とバージョンガードにより、既存コードへの影響を最小限に抑えつつ、将来的な例外種別追加にも柔軟に対応できる設計となっています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
0137168c

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文→背景→技術的変更→設計判断→まとめの5部構成で、総論・各論・結論が明確に分かれている。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライトは正しい形式(```ruby:filepath`)で使用され、PRリンクも正しくMarkdownリンク化されている。

対象読者への適合性 ✓ PASS

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

専門エンジニア向けの高度な内容で、余計な基礎解説はなく適切。

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

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

各セクションが総論・各論・結論の段落構成になり、トピックセンテンスで始まり、1段落1トピックで適切な長さに保たれている。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックはDiffと完全に一致し、追加されたFAILSAFE_ERRORS定数やrescue文が正確に反映されている。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている用語はPRで使われているものと一致し、誤用はない。

説明の技術的正確性 ✓ PASS

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

技術的な説明はPRの背景・変更点と合致し、因果関係も正しく記述されている。

事実の突合 ✓ PASS

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

すべての主張がPRのタイトル・説明・Diffで裏付けられており、推測や捏造はない。

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

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

PR番号 #57476 などの数値は正確。

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

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

記事タイトルはPRタイトルの要旨を正しく反映している。

外部知識の正確性 ✓ PASS

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

記事に外部知識(LTS、リリース予定等)は含まれていない。

時間表現の正確性 ✓ PASS

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

時間表現の記載はなく、PR情報と齟齬もない。