`ActionDispatch::ExceptionWrapper` をRactor対応に変更し、設定APIを追加
ActionDispatch::ExceptionWrapper のクラス変数をすべてfreezeし、設定変更を新しい凍結オブジェクトへの再代入で行うことでRactor安全にしました。あわせて wrapper_exceptions と silent_exceptions の公開設定APIが追加されています。
背景
Ractor では、可変な共有状態(mutable shared state)を複数のRactorから操作することが禁止されています。変更前の ActionDispatch::ExceptionWrapper は、rescue_responses や rescue_templates などのクラス変数としてHashやArrayを可変のまま保持しており、初期化フェーズで merge! などの破壊的メソッドで書き換える設計でした。この構造は、将来Ractorを利用するアプリケーションにとって安全でない共有状態を持つことになるため、Ractor対応が必要でした。
rescue_responses と rescue_templates はエンジン側の設定(config.action_dispatch.rescue_responses / config.action_dispatch.rescue_templates)として既に公開されていましたが、wrapper_exceptions と silent_exceptions は公開設定が存在しませんでした。今回のPRはRactor安全化と同時に、この2つの設定APIを追加しています。
技術的な変更
exception_wrapper.rb での変更はシンプルで、4つのクラス変数のデフォルト値すべてに .freeze を追加することで、オブジェクト自体を不変にしています。
変更前:
cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!(
...
)
cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
...
)
cattr_accessor :wrapper_exceptions, default: [
"ActionView::Template::Error"
]
cattr_accessor :silent_exceptions, default: [
"ActionController::RoutingError",
"ActionDispatch::Http::MimeNegotiation::InvalidType"
]
変更後:
cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!(
...
).freeze
cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
...
).freeze
cattr_accessor :wrapper_exceptions, default: [
"ActionView::Template::Error"
].freeze
cattr_accessor :silent_exceptions, default: [
"ActionController::RoutingError",
"ActionDispatch::Http::MimeNegotiation::InvalidType"
].freeze
デフォルト値をfreezeしたことにより、railtie.rb でのアプリケーション設定の適用方法も変わっています。破壊的な merge! による書き換えから、新しい凍結オブジェクトへの再代入に切り替えています。
変更前:
ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses)
ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates)
変更後:
ActionDispatch::ExceptionWrapper.rescue_responses = ActionDispatch::ExceptionWrapper.rescue_responses.merge(config.action_dispatch.rescue_responses).freeze
ActionDispatch::ExceptionWrapper.rescue_templates = ActionDispatch::ExceptionWrapper.rescue_templates.merge(config.action_dispatch.rescue_templates).freeze
ActionDispatch::ExceptionWrapper.wrapper_exceptions = (ActionDispatch::ExceptionWrapper.wrapper_exceptions | config.action_dispatch.wrapper_exceptions).freeze
ActionDispatch::ExceptionWrapper.silent_exceptions = (ActionDispatch::ExceptionWrapper.silent_exceptions | config.action_dispatch.silent_exceptions).freeze
Hashには非破壊的な merge(戻り値を使う)、Arrayには集合の和を求める | 演算子を使い、いずれも結果を freeze してクラス変数へ再代入しています。この初期化フェーズでの一度限りの再代入により、アプリケーション起動後はすべてのクラス変数が不変オブジェクトとなります。
また railtie.rb には、新しく追加された2つの設定のデフォルト値(空のコレクション)も追加されています。
config.action_dispatch.wrapper_exceptions = []
config.action_dispatch.silent_exceptions = []
設計判断
既存の設定キーを再代入で更新する方式 が採用されました。オブジェクトを直接変更する代わりに、新しいオブジェクトを生成して代入することでRactor安全性を確保しています。この設計では、初期化タイミング(initializer ブロック)が一度しか実行されないことが前提であり、起動完了後はクラス変数が変化しないことが保証されます。
アプリケーションから設定を追加する際は、+= や |= ではなく設定ファイルを通じた公式APIを利用することが想定されています。wrapper_exceptions と silent_exceptions については config.action_dispatch.wrapper_exceptions += [MyException] のように既存のデフォルト値を保持しながら追加できます(| 演算子による結合のため、重複は排除されます)。
なお、rescue_responses の場合は Hash#merge の結合方式(後者優先)、wrapper_exceptions / silent_exceptions の場合は Array#| の集合和で結合方式が異なります。これはそれぞれのデータ構造の特性を活かした適切な選択です。
まとめ
この変更は、デフォルトオブジェクトのfreeze化と設定適用時の再代入方式への切り替えという最小限の変更で、ActionDispatch::ExceptionWrapper をRactor対応にしています。同時に wrapper_exceptions と silent_exceptions の設定APIが公開されたことで、フレームワークやプラグインレベルの例外に対するバックトレース表示のカスタマイズが、従来の内部API操作ではなく公式の設定体系で行えるようになります。