Railsアプリの例外処理をRackミドルウェアで
コントローラ内で発生する例外であれば、rescue_from などを利用した例外処理を行える。
一方、データベースサーバに障害が起きた場合などはコントローラの外側で例外が処理され、コントローラの rescue_from で宣言した例外処理は使われない。
今回は、アプリケーションの外側で発生する例外を処理する方法について整理してみる。
前提
例外処理には以下の Rack アプリケーションを使用する。
# app/controllers/database_failure.rb class DatabaseFailure < ActionController::Metal include ActionController::Redirecting def self.call(env) action(:maintenance).call(env) end def maintenance redirect_to '/maintenance.html' end end # app/controllers/exceptions_app.rb class ExceptionsApp < ActionController::Metal include ActionController::Rendering def self.call(env) action(:handle_exception).call(env) end def handle_exception render file: 'public/maintenance.html', status: :service_unavailable end end
ActionDispatch::ShowExceptions
config.exceptions_app で指定した Rack アプリケーションで例外を処理させることができる。
デフォルトでは config.exceptions_app は未指定で、その場合は ActionDispatch::PublicExceptions.new(Rails.public_path) が使われる。
# config/application.rb module Sample class Application < Rails::Application ... config.exceptions_app = ->(env) do case env["action_dispatch.exception"] when Mysql2::Error DatabaseFailure.call(env) else ExceptionsApp.call(env) end end end end
※ development モードなど、config.consider_all_requests_local が真である場合は働いてくれない様なので使いどころに気をつけた方が良いかも。
ActionDispatch::Rescue
ミドルウェアスタックに ActionDispatch::Rescue を差し込む事で、任意の Rack アプリケーションに例外を処理させることが出来る。
module Sample class Application < Rails::Application ... config.middleware.insert_before ActiveRecord::ConnectionAdapters::ConnectionManagement, ActionDispatch::Rescue do rescue_from Mysql2::Error, DatabaseFailure end end end
※ config.consider_all_requests_local に関わらず働いてくれる。
config.ru (暫定)
Rails の初期化処理で例外が発生した場合に、任意の例外処理を実行したい場合にどうしたらよいのか。
例えば、config.ru を以下の様に書いたら良いだろうか。
run ->(env) do begin require ::File.expand_path('../config/environment', __FILE__) Sample::Application.call(env) rescue Mysql2::Error DatabaseFailure.call(env) end end
これは上手くいかない。初期化処理が例外によって中断されるため、次のリクエストを処理する際にも初期化処理が実行されてしまい、意図しない例外が発生してしまう。
初期化処理を2回実行してしまう問題を解決する方法がまだ見つけられてない。
追試(1): DelayedJob を使うと初期化で例外終了する件
MySQL サーバ停止後に素の Rails アプリで passenger start すると、インスタンス起動時に上手く指定した例外処理を実行出来る。
そこで、gem 'delayed_job_active_record' として DelayedJob を使い始めると、インスタンス起動時に例外終了する様になる。
例外が発生するのは、バックトレースを見る限り ActiveRecord::Base.clear_cache! の部分。
initializer "active_record.set_reloader_hooks" do |app| hook = lambda do ActiveRecord::Base.clear_reloadable_connections! ActiveRecord::Base.clear_cache! end if app.config.reload_classes_only_on_change ActiveSupport.on_load(:active_record) do ActionDispatch::Reloader.to_prepare(&hook) end else ActiveSupport.on_load(:active_record) do ActionDispatch::Reloader.to_cleanup(&hook) end end end