Engineのルート初期化を遅延させ、遅延ルートセットの最適化を確実に適用する
Engine#routesにブロックを渡すタイミングによって、LazyRouteSetが設定される前にルートオブジェクトが生成されてしまう問題が修正されました。これにより、#52353で導入されたブート時のルート描画遅延最適化が、graphqlgem等のEngineでも正しく機能するようになります。
背景
#52353は、ルートの読み込みを最初のリクエスト時まで遅延させることで、大規模アプリのブート時間を短縮する最適化を導入しました。この最適化の核心は、config.route_set_classにLazyRouteSetを割り当てる:make_routes_lazyイニシャライザです。
しかし、Engineの定義内でroutes doを直接呼び出すgemが存在します。graphql-rubyのDashboardEngineがその典型例です。このようなgemは、Railsのイニシャライザが実行される前にclassボディの評価時点でroutes doを呼び出すため、config.route_set_classがまだデフォルトのRouteSetを指している段階でルートオブジェクトが生成されていました。結果として:make_routes_lazyが後からLazyRouteSetを設定しても、すでに生成済みのルートオブジェクトには影響せず、最適化が完全に無効化されていました。
技術的な変更
Engine#routesの実装が拡張され、ブロックを渡されたタイミングと@routesオブジェクトの初期化タイミングが分離されました。
変更前:
def routes(&block)
@routes ||= config.route_set_class.new_with_config(config)
@routes.append(&block) if block_given?
@routes
end
変更後:
def routes(&block)
if block_given?
if @route_blocks
@route_blocks << block
else
@routes ||= config.route_set_class.new_with_config(config)
@routes.append(&block)
end
elsif @routes.nil?
@routes = config.route_set_class.new_with_config(config)
if @route_blocks
blocks, @route_blocks = @route_blocks, nil
blocks.each { |b| routes(&b) }
end
end
@routes
end
新たに導入された@route_blocksは、ルートブロックを一時的にバッファリングするための配列です。@routesがまだ初期化されていない段階でブロックが渡された場合、ブロックは即座にappendされるのではなく@route_blocksに蓄積されます。
加えて、:make_routes_lazyイニシャライザの末尾でroutes(引数なし)を明示的に呼び出すようになりました。
initializer :make_routes_lazy, before: :bootstrap_hook do |app|
config.route_set_class = LazyRouteSet if Rails.env.local?
routes
end
このroutesの呼び出しが@routes.nil?ブランチを起動し、config.route_set_classが確定した後に@routesオブジェクトを生成し、@route_blocksに蓄積されていたブロックをすべてappendして@route_blocksをクリアします。
設計判断
「ブロックの受け付け」と「ルートオブジェクトへの適用」を分離するアプローチが採用されました。
この設計の巧みな点は、@route_blocksの存在そのものを「まだ初期化フェーズ前である」というフラグとして利用していることです。@route_blocksがnilかつ@routesもnilの場合は、これまで通りの即時初期化パスを通ります。一方、@route_blocksが配列として存在する場合は蓄積モードに入ります。blocks, @route_blocks = @route_blocks, nilという一行での「取り出しと同時にリセット」は、再帰的な呼び出しによる二重適用を防ぐための工夫です。
また、:make_routes_lazyからroutesを呼び出す設計は、@route_blocksが空の場合(ブロックが一度も渡されていない場合)も@routesを初期化する副作用を持ちます。これはイニシャライザの実行順序に依存した確定的な初期化を保証するものです。
まとめ
Engineのルートブロック評価をroute_set_classの設定完了後まで遅延させることで、クラス定義時にroutes doを呼び出すgemでも遅延ルートセット最適化が正しく機能するようになりました。@route_blocksバッファという最小限の状態追加により、既存のroutesメソッドのインターフェースを変えずに初期化順序の問題を解消した変更といえます。