フレームワーク基底クラスの先行ロードによるViewReloader初期化問題の解消
SpringのpreloadフェーズでActionMailer::Base・ActionController::Base・ActionController::APIを事前にロードすることで、ActionViewのCacheExpiry::ViewReloaderが半初期化状態で固定される問題を解消します。これにより、各forkでのprepend_view_pathが引き起こしていた不要なFileUpdateCheckerの再構築コストが排除されます。
背景
この問題は、SpringのpreloadフェーズとRailsの遅延初期化コントラクトの間の競合として発生します。Spring::Application#serveはfork前にRails.application.reloaders.any?(&:updated?)を呼び出してリロードの要否を確認しますが、このタイミングでActionViewのCacheExpiry::ViewReloader#updated?が初めて実行されると、その時点で登録されているviewパスをもとにFileUpdateCheckerを構築します。
問題の核心は、各Engineがprepend_view_pathを呼び出すのがActiveSupport.on_load(:action_mailer) / :action_controller_baseフック経由であるという点にあります。Springのpreloadフェーズではこれらの基底クラスがまだ参照されていないため、on_loadフックが発火しておらず、dirs_to_watchが空の状態でreloaderのprobeが実行されます。rails/rails#51308が実装した遅延初期化では「@watcherがnilのままviewパスが登録されるまで待つ」という契約を前提にしていますが、dirs_to_watchが空の配列のままupdated?が呼ばれると、空のFileUpdateCheckerが構築されて@watcherに代入されてしまいます。
一度@watcherが非nilになると、#rebuild_watcher内のreturn unless @watcherガードが開いた状態になります。その後の各Engineによるprepend_view_pathのたびに、累積されたディレクトリセット全体に対してDir[*globs] + File.mtimeの完全な再スキャンが走り続けます。rails/rails#57269がRails側からこのバグを修正していますが、本PRはSpring側からも独立して意味のある対策となります。
技術的な変更
lib/spring/application.rbのpreloadメソッドにpreload_framework_base_classesの呼び出しを追加し、preloadフェーズの末尾(invoke_after_environment_load_callbacksの直後)でフレームワーク基底クラスを事前にロードします。
追加されたメソッドと定数は次のとおりです:
FRAMEWORK_BASE_CLASSES = %w[
ActionMailer::Base
ActionController::Base
ActionController::API
].freeze
def preload_framework_base_classes
FRAMEWORK_BASE_CLASSES.each do |const|
Object.const_get(const) if Object.const_defined?(const)
end
end
そしてpreloadメソッド内の呼び出し箇所:
def preload
require Spring.application_root_path.join("config", "environment")
invoke_after_environment_load_callbacks
preload_framework_base_classes # ← 追加
disconnect_database
# ...
end
Object.const_defined?(const)で存在確認を行ってからObject.const_get(const)でロードする実装により、ActionMailerやActionController::APIを使用しないアプリケーションでもエラーにならない安全な設計になっています。これら3クラスへの参照が親プロセスで発生することで、ActiveSupport.on_load(:action_mailer) / :action_controller_baseフックが親プロセスのpreload中に発火し、Engineがprepend_view_pathを正常に登録した状態でforkが行われます。
設計判断
アプリケーション名前空間のクラス(Application*)を除外し、フレームワーク基底クラスのみを対象とする設計が採用されています。PR本文にあるRafael's suggestionに従い、フレームワークコードは再ロードされないが、アプリケーション名前空間は再ロード対象であるという区別に基づいた判断です。アプリケーション固有のクラスをSpringのpreload対象に含めると、ユーザーコードの変更検知ロジックとの結合が生じるため、基底クラスのみに限定しています。
定数名を文字列の配列FRAMEWORK_BASE_CLASSESとして定数化しているのも注目点です。直接クラス参照(ActionMailer::Base)を使わず文字列を経由することで、これらのgemがインストールされていない環境でのロード時エラーを避けられます。const_defined?チェックとの組み合わせにより、API-onlyアプリケーション(ActionMailerなし)でも安全に動作します。
まとめ
本PRは、Springのpreload処理の末尾にわずか数行の定数定義とメソッドを追加するだけで、「空のFileUpdateCheckerが固定される競合」という根本原因をSpring側から直接断ち切っています。Rails側のrails/rails#57269と相補的に機能し、双方が適用されることでviewパスの監視機構が確実に正しい初期化順序をたどれるようになります。