キャッシュのデシリアライズエラーをcache missとして扱う安全な実装
Railsでは、キャッシュバックエンドから破損したデータが返された場合に、デシリアライズエラーをキャッシュミスとして扱う仕組みが導入されました。memcachedなどのバックエンドで稀に発生する切り詰められたレスポンスに対して、アプリケーションがクラッシュせずに動作を継続できるようになります。
背景
キャッシュバックエンドは通常安定していますが、memcachedなどでネットワーク障害やメモリ圧迫時に切り詰められたレスポンスを返すことがあります。従来の実装では、このような破損データのデシリアライズ時に発生する例外が適切にハンドリングされず、アプリケーション全体に影響を与える可能性がありました。
キャッシュの本質的な役割から考えると、デシリアライズに失敗した場合はキャッシュミスとして扱い、元のデータソースから値を再取得するのが適切な動作です。#56729 はこの原則に基づいた実装を提供しています。
技術的な変更
ActiveSupport::Cache::Coder の load メソッドと LazyEntry の value メソッドに、エラーハンドリングが追加されました。
load メソッドの変更
変更前:
def load(dumped)
return @serializer.load(dumped) if !signature?(dumped)
# ...
end
変更後:
def load(dumped)
unless signature?(dumped)
return begin
@serializer.load(dumped)
rescue => error
ActiveSupport.error_reporter.report(error, source: "active_support.cache")
end
end
# ...
end
シグネチャを持たない従来形式のキャッシュデータのデシリアライズ時に例外が発生した場合、エラーレポーターに報告した上で nil を返します。これにより、破損データはキャッシュミスとして扱われます。
LazyEntry の value メソッドの変更
変更前:
def value
if !@resolved
@value = @serializer.load(@compressor ? @compressor.inflate(@value) : @value)
@resolved = true
end
@value
end
変更後:
def value
if !@resolved
@value = begin
@serializer.load(@compressor ? @compressor.inflate(@value) : @value)
rescue => error
ActiveSupport.error_reporter.report(error, source: "active_support.cache")
raise DeserializationError, error.message
end
@resolved = true
end
@value
end
LazyEntry はキャッシュエントリの遅延評価を実現するクラスです。値へのアクセス時にデシリアライズエラーが発生した場合、エラーレポーターへの報告後に DeserializationError を発生させます。この例外は上位層でキャッチされ、キャッシュミスとして処理されます。
SerializerWithFallback の圧縮解除エラーハンドリング
if marked.start_with?(MARK_COMPRESSED)
dumped = begin
Zlib::Inflate.inflate(dumped)
rescue Zlib::Error => error
raise Cache::DeserializationError, "#{error.class}: #{error.message}"
end
end
SerializerWithFallback では、圧縮データの解凍時に発生する Zlib::Error を DeserializationError に変換します。zlibの低レベルエラーをキャッシュ層の統一されたエラー型に変換することで、上位層での一貫したエラーハンドリングを可能にしています。
設計判断
エラーレポーターへの報告とキャッシュミスへの変換 という二段階のアプローチが採用されました。
単純に例外を握りつぶすのではなく、ActiveSupport.error_reporter.report で問題を記録した上で、nil を返すかキャッシュ層の例外に変換しています。これにより、運用者はデシリアライズエラーの発生頻度や原因を監視できる一方、アプリケーションは正常に動作を継続できます。
DeserializationError という新しい例外クラスの導入により、キャッシュ層特有のエラーと他の例外を区別できるようになりました。上位層ではこの例外を捕捉してキャッシュミスとして扱うことで、元のデータソースからの再取得という適切なフォールバック動作を実現しています。
テストコードでは assert_error_reported ヘルパーを使用して、エラーレポーターへの報告が確実に行われることを検証しています。これは、デシリアライズエラーの発生が運用上の可視性を持つべきという設計意図を反映しています。
本PRは、キャッシュシステムの堅牢性を高める実装です。破損データによるアプリケーションクラッシュを防ぎつつ、エラーレポーターによる監視可能性を維持することで、キャッシュバックエンドの一時的な不具合に対する耐性を向上させています。