RedisCacheStoreのフェイルセーフでRedisClient::Errorを捕捉
ActiveSupport::Cache::RedisCacheStore は Redis が利用不可でも例外を上げずにフォールトトレラントとして動作することがドキュメントで保証されています。本 PR では、特定の Sentinel 構成下で発生する RedisClient::Error 系列が既存のフェイルセーフで捕捉されず、アプリケーションエラーに転嫁されていた問題を修正し、例外安全性を再度保証します。
背景
RedisCacheStore は内部で failsafe ラッパーを通して Redis::BaseError や ConnectionPool::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 がスローされた際に全ての公開キャッシュメソッドがフェイルセーフのデフォルトを返すことを検証しています。テスト用ダブル RedisClientErrorRedisClient は ensure_connected で RedisClient::Error を発生させ、translate_error! を No‑op にオーバーライドして Sentinel パスを忠実に再現します。
結果的に、新しい例外が捕捉対象に加わり、既存の動作に影響を与えることなくフォールトトレラント機構が強化されました。
設計判断
例外リストの拡張を 定数ベース で実装する選択は、将来的な例外種別追加に対する拡張性を高めます。defined? ガードを用いることで、redis‑rb のメジャーバージョン間の API 差異をコードレベルで吸収し、後方互換性 を維持しつつ新しい例外を安全にハンドリングできます。
代替案としては、failsafe 内で個別に rescue ::RedisClient::Error を追加するや、設定フラグで例外種別を切り替える方法が考えられましたが、定数集合に集約することで例外ハンドリングロジックが一元化され、可読性と保守性が向上します。また、テスト側でクライアント実装を差し替える手法を採用した点は、実装依存を最小化しながら実環境に近いシナリオを再現できる実務的なデザインです。
総合的に、この設計は「例外捕捉の網羅性を保ちつつ、ライブラリのバージョン多様性に耐える」ことを最優先した結果と言えます。
まとめ
今回の PR は RedisCacheStore のフェイルセーフに RedisClient::Error を加えることで、Sentinel 構成下でも例外が漏れずにフォールトトレラントが保証されるようにしました。定数化とバージョンガードにより、既存コードへの影響を最小限に抑えつつ、将来的な例外種別追加にも柔軟に対応できる設計となっています。