`after_environment_load`フックでSpringのCoWプリロードが可能に
Springに Spring.after_environment_load フックが追加され、Railsアプリケーションの環境ロード直後にカスタムコードをSpringサーバープロセスへプリロードできるようになりました。これにより、フォークされたワーカーがCopy-on-Write(CoW)でメモリを共有し、テスト実行ごとの初期化コストを削減できます。
背景
SpringはRailsアプリケーション環境をサーバープロセスに保持し、テスト実行時にそのプロセスをフォークすることで起動コストを削減します。しかし従来、test_helper のような重い初期化コードはフォーク後の各ワーカーが毎回実行する必要がありました。
Shopifyのモノリスでは、bin/test を実行するたびに test_helper の require に約2.4秒かかっていました。Springがホット状態であっても、この2.4秒は毎回支払われ続けていました。OSのCoWセマンティクスを活用するには、フォーク前のサーバープロセスに対象コードをロード済みにする必要があります。
この問題を解決するため、環境ロード完了後かつフォーク待機ループ開始前という適切なタイミングで実行されるフックが必要でした。
技術的な変更
変更は Spring::Configuration と Spring::Application の2ファイルに対して行われ、既存の after_fork フックと対称的な設計で実装されています。
lib/spring/configuration.rb に、コールバックの登録・保持メソッドが追加されました。
def after_environment_load_callbacks
@after_environment_load_callbacks ||= []
end
def after_environment_load(&block)
after_environment_load_callbacks << block
end
@after_environment_load_callbacks は遅延初期化される配列で、after_fork_callbacks と全く同じパターンです。
lib/spring/application.rb では、preload メソッド内の require app_env 呼び出し直後にコールバック起動が挿入されました。
# 変更前
require Spring.application_root_path.join("config", "environment")
disconnect_database
# 変更後
require Spring.application_root_path.join("config", "environment")
invoke_after_environment_load_callbacks
disconnect_database
invoke_after_environment_load_callbacks はプライベートメソッドとして追加されており、登録されたコールバックを順番に call します。注目すべきは実行タイミングで、GC.compact / Process.warmup よりも前に実行されます。プリロードしたオブジェクトもコンパクション・ウォームアップの対象に含まれるため、メモリ効率がさらに高まります。
利用側のAPIは config/spring.rb に記述します。
Spring.after_environment_load do
require "test_helper"
end
テストは test/support/acceptance_test.rb と新規追加の test/unit/configuration_test.rb の両方でカバーされています。受け入れテストでは、コールバック内でロードした test_helper を変更した場合にSpringが正しくリロードを検知することも検証されています。
設計判断
after_fork と対称的な設計が採用されました。コールバック配列の遅延初期化、登録メソッドのシグネチャ、呼び出しメソッドの命名規則がすべて after_fork パターンと一致しており、既存コードを読んでいたエンジニアにとって学習コストがありません。
フックはSpringが無効化されているときや環境がリロードされるときには呼び出されない仕様です。これは意図的な設計であり、「Springサーバーの初回ブートでのみ1回実行する」という明確なセマンティクスを保ちます。リロード時に再実行されないことで、ファイル変更検知からリロードまでの時間を余分に遅延させません。
コールバックが disconnect_database より前に実行される点も重要です。プリロードするコード(test_helper など)がデータベース接続を確立する場合、その接続は disconnect_database によってクリーンアップされ、フォークされたワーカーが古い接続を引き継ぐ問題を回避できます。
まとめ
after_environment_load フックは、SpringのCoWアーキテクチャを最大限に活用するための仕組みです。after_fork との一貫したAPIと、GC.compact 前・disconnect_database 前という精密な実行タイミングの設計により、重いテストインフラのプリロードを安全かつ効果的にSpringサーバーへ委譲できるようになりました。