ロードフック実行前のアプリケーション初期化チェック機能を追加
Railsに、ActiveSupport.on_loadフックがアプリケーション初期化前に実行されることを検知する仕組みが導入されました。これにより、フレームワークの早期ロードによる起動時間の低下やロード順序の競合を防ぐことができます。
背景
Railsアプリケーションでは、gemがロードフックの外でフレームワークを直接参照することで、意図せずフレームワークを早期にロードしてしまう問題が発生していました。例えば、ActiveRecord::Baseをinitializer内で直接呼び出すと、アプリケーションの初期化完了前にActive Recordが読み込まれます。この早期ロードは起動時間を遅延させ、ロード順序の競合を引き起こす可能性がありました。
#46047では警告機能の実装が提案されていましたが、本PRはより包括的なガード機能として実装されました。従来は、開発者が独自にスクリプトを書いてこの問題を検出する必要がありました:
require "bundler/setup"
require "active_support/lazy_load_hooks"
ActiveSupport.on_load(:active_record) do
raise "Oops!" # アプリケーション起動前に実行されると例外が発生
end
require_relative "config/environment"
技術的な変更
Rails::Railtieにguard_load_hooksメソッドが追加され、各フレームワークのRailtie/Engineでこのメソッドを呼び出してロードフックの監視を設定できるようになりました。
変更の例(Active Recordの場合):
guard_load_hooks(
:active_record, :active_record_encryption, :active_record_fixture_set, :active_record_fixtures,
:active_record_mysql2adapter, :active_record_postgresqladapter, :active_record_sqlite3adapter,
:active_record_trilogyadapter,
)
各ロードフックには、以下のロジックで早期実行を検出するガードが追加されます:
def guard_load_hooks(*components)
components.each do |component|
ActiveSupport.on_load(component) do
if Rails.try(:application) && !Rails.configuration.eager_load && !Rails.application.initialized?
case Rails.configuration.action_on_eary_load_hook
when :log
(Rails.logger || ActiveSupport::Logger.new($stdout)).warn <<~MSG
#{Railtie.load_hook_guard_message_for(component)}
Called from:
#{caller.join("\n")}
MSG
when :raise
raise LoadError, Railtie.load_hook_guard_message_for(component)
end
end
end
end
end
Railtie.load_hook_guard_message_for(component)ヘルパーメソッドは、以下の警告メッセージを生成します:
:active_record was loaded before appliction initialization.
Prematurely executing load hooks will slow down your boot time
and could cause conflicts with the load order of your application.
Please wrap your code with an on_load hook:
ActiveSupport.on_load(:active_record) do
# your code here
end
チェック条件は以下の3つです:
-
Rails.applicationが存在する(Railsアプリケーションが起動中) -
!Rails.configuration.eager_load(Eager Loadモードでない) -
!Rails.application.initialized?(アプリケーションの初期化が完了していない)
これらすべてを満たす場合に、設定に応じて警告またはエラーを発生させます。
設定オプション
config.action_on_eary_load_hookが新たに追加され、早期ロードの検出時の動作を制御できます:
@action_on_eary_load_hook = :log # デフォルト値
設定可能な値は2種類です:
-
:log(デフォルト): 警告メッセージをログに出力します。既存のアプリケーションでも安全に有効化できます。 -
:raise:LoadErrorを発生させます。CI環境での早期検出に有用です。
警告メッセージには、問題のあるロードフックの名前、影響の説明、修正方法の提案、そして呼び出し元のスタックトレースが含まれます。開発者は、提案されたActiveSupport.on_loadブロックでコードをラップすることで、適切なタイミングでの実行を保証できます。
テストの更新
既存のテストコードでは、Active Recordの設定を直接initializer内で実行していたため、警告が発生するようになりました。これらはActiveSupport.on_load(:active_record)ブロックでラップされました:
変更前:
app_file "config/initializers/active_record.rb", <<-RUBY
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define(version: 1) do
create_table :posts do |t|
t.string :title
end
end
RUBY
変更後:
app_file "config/initializers/active_record.rb", <<-RUBY
ActiveSupport.on_load(:active_record) do
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define(version: 1) do
create_table :posts do |t|
t.string :title
end
end
end
RUBY
この変更により、テストコード自体が正しいロードフックの使用例を示すことになりました。
対象となるロードフック
以下のフレームワークとコンポーネントでロードフックガードが設定されました:
-
Action Cable:
:action_cable,:action_cable_channel,:action_cable_connection,:action_cable_test_case,:action_cable_connection_test_case -
Action Mailbox:
:action_mailbox_inbound_email,:action_mailbox_record,:action_mailbox,:action_mailbox_test_case -
Action Mailer:
:action_mailer,:action_mailer_test_case -
Action Controller:
:action_controller,:action_controller_base,:action_controller_api,:action_controller_test_case -
Action Dispatch:
:action_dispatch_request,:action_dispatch_response,:action_dispatch_system_test_case,:action_dispatch_integration_test -
Action Text:
:action_text_record,:action_text_rich_text,:action_text_content,:action_text_encrypted_rich_text -
Action View:
:action_view,:action_view_test_case -
Active Job:
:active_job,:active_job_arguments,:active_job_continuable,:active_job_test_case -
Active Model:
:active_model,:active_model_error,:active_model_secure_password,:active_model_translation -
Active Record:
:active_record,:active_record_encryption,:active_record_fixture_set,:active_record_fixtures, アダプター関連のフック -
Active Storage:
:active_storage_record,:active_storage_attachment,:active_storage_blob,:active_storage_variant_record -
Active Support:
:message_pack,:active_support_test_case
これらすべてのロードフックで、早期実行が監視されるようになりました。
設計判断
Railtieレベルでの実装が選択されました。guard_load_hooksをRails::Railtieのクラスメソッドとして実装することで、各フレームワークやgemが独自のRailtie/Engineで簡単にガード機能を追加できます。これにより、サードパーティのgemも同じ仕組みを利用できます。
デフォルトは:logに設定されました。既存のアプリケーションで突然エラーが発生することを避けつつ、問題を可視化できます。開発者は必要に応じて:raiseに変更し、CI環境で早期ロードを厳格にチェックすることができます。
Eager Loadモードでは無効化される設計です。!Rails.configuration.eager_loadの条件により、本番環境のEager Loadモードでは警告が発生しません。Eager Loadではすべてのフレームワークが意図的に事前ロードされるため、このチェックは不要です。
まとめ
本PRは、Railsアプリケーションにおけるフレームワークの早期ロード問題に対する包括的なガード機能を導入しました。guard_load_hooksメソッドにより、各フレームワークのロードフックに監視機能が追加され、開発者は早期ロードの問題をログまたはエラーで検出できるようになりました。デフォルトの:log動作により既存のアプリケーションへの影響を最小限に抑えつつ、:raiseオプションでCI環境での厳格なチェックも可能です。この機能は、起動時間の改善とロード順序の問題の早期発見に貢献します。