フォークプロセスでのビューリローダーフック漏れを修正する `ReloadersCollection`

rails/rails

ViewReloader#deactivate と新設の ReloadersCollection により、Spring などのフォークプロセスが app.reloaders.clear を呼んだ際に、ファイルシステムリゾルバのフックも確実に除去されるようになりました。これにより、不要な Dir.[] + File.mtime スキャンがフォーク後のワーカープロセスで繰り返し発生する問題が解消されます。

背景

ViewReloader は railtie 初期化時に PathRegistry.file_system_resolver_hooksrebuild_watcher フックを登録しますが、フォークプロセスがリローダーを破棄する際にこのフックが残留するという問題がありました。Spring のテストワーカーは app.reloaders.clear で配列からリローダーを削除しますが、フックは file_system_resolver_hooks に残り続けます。その結果、フォーク後のワーカーで prepend_view_path が呼ばれるたびに、全ビューディレクトリに対してファイルスキャンが実行されていました。

本PRは #57269 のフォローアップです。#57269 は早期ブート時の空ウォッチャー構築を防いだ変更であり、本PRはさらに踏み込んで、フォークワーカーでファイル監視が不要な場合にフック自体を完全に除去する仕組みを導入しています。

この問題の影響はフォークプロセスに限らず、prepend_view_path が呼ばれるたびにスキャンが走るため、エンジンの on_load コールバックが多い大規模アプリケーションほど顕著に現れます。#57269 の計測では、Shopify のモノレポにて Spring ウォームな状態での prepend_view_path × 10 回が10回の実リビルドから0回に削減された事例が示されています。

技術的な変更

変更は大きく2つの軸で構成されています。ViewReloader への deactivate メソッドの追加と、app.reloaders のコレクション型の刷新です。

ViewReloader への create ファクトリと deactivate の導入

actionview/lib/action_view/cache_expiry.rbViewReloader.create クラスメソッドと deactivate インスタンスメソッドが追加されました。

変更前:

view_reloader = ActionView::CacheExpiry::ViewReloader.new(watcher: app.config.file_watcher)
ActionView::PathRegistry.file_system_resolver_hooks << view_reloader.method(:rebuild_watcher)

変更後:

view_reloader = ActionView::CacheExpiry::ViewReloader.create(watcher: app.config.file_watcher)

create の内部では、フック登録をインスタンス生成と一体化させています。また、initialize でフックを @hook = method(:rebuild_watcher) として保持することで、後から同一の Method オブジェクトを参照できるようにしています。

def self.create(watcher:, &block)
  reloader = new(watcher: watcher, &block)
  ActionView::PathRegistry.file_system_resolver_hooks << reloader.hook
  reloader
end

def deactivate
  ActionView::PathRegistry.file_system_resolver_hooks.delete(@hook)
  @watcher = nil
  @watched_dirs = nil
end

attr_reader :hook

deactivate@watcher@watched_dirs もリセットするため、仮にインスタンスが参照され続けても再スキャンは発生しません。Array#delete の同一性判定は Method オブジェクトの == を使うため、method(:rebuild_watcher)@hook に固定して保持する設計が重要です。

ReloadersCollection の新設

railties/lib/rails/application/reloaders_collection.rb に新しいクラスが追加され、app.reloaders の型が従来の Array から ReloadersCollection に変更されました。

class ReloadersCollection
  include Enumerable

  def clear
    @reloaders.each { |r| r.deactivate if r.respond_to?(:deactivate) }
    @reloaders.clear
  end

  def delete(reloader)
    reloader.deactivate if reloader.respond_to?(:deactivate)
    @reloaders.delete(reloader)
  end
end

ReloadersCollectionEnumerable をインクルードし、<<sizeempty?each を実装することで、既存の Array としての利用箇所に対して互換性を維持しています。clear および delete の際に respond_to?(:deactivate) でガードしているため、deactivate を持たない既存のリローダーもそのまま混在できます。

# 変更前
@reloaders = []

# 変更後
@reloaders = ReloadersCollection.new

app.reloaders を直接操作している既存コードは、Enumerable のインターフェースを通じている限り変更不要です。

設計判断

フック除去の責任をコレクション側に持たせた ことで、呼び出し側は clear / delete を従来通り呼ぶだけでクリーンアップが自動的に実行されます。Spring の app.reloaders.clear 呼び出しはそのままで、追加の変更なしに恩恵を受けられます。

deactivaterespond_to? で検査するオプトイン設計が採用されています。これにより、deactivate を実装していない既存のリローダー(例: ルートリローダーなど)を ReloadersCollection に混在させても安全に動作します。インターフェースを強制するのではなく、能力の有無を実行時に判定するアプローチは、後方互換性と拡張性のバランスを取る判断です。

また、フック登録を initialize から切り離し create ファクトリメソッド に移した点も注目です。initialize でフックを登録すると、テストコードが new を使った際に意図せずグローバル状態を汚染します。テストでは引き続き new を使って副作用なしにインスタンスを生成でき、create を呼んだ場合のみフック登録が行われます。

まとめ

本PRは、ViewReloader のライフサイクルとグローバルフックの整合性という、見落とされがちな問題に対処しています。ReloadersCollection という薄いラッパーを導入することで、既存の app.reloaders.clear という呼び出しパターンを変えることなく、リローダーの破棄とフックのクリーンアップを原子的に結びつける設計を実現しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
36c070b0

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という理想的な3部構成が明確に守られています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(`言語:ファイルパス`)およびGitHubのPRリンク記法(`[#123](URL)`)が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Railsの内部構造に関する専門用語が適切に使用されており、専門知識を持つエンジニアという対象読者に合致した内容と技術レベルです。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。可読性が非常に高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiff情報と正確に一致しています。ファイルパス、メソッドの追加・変更箇所も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`ViewReloader`, `file_system_resolver_hooks`, `ReloadersCollection`などの技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

フックが残留する問題、`Method`オブジェクトの同一性、`respond_to?`による後方互換性の担保など、技術的な説明はすべて正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PR Description、関連PR(#57269)、およびDiff内のコードによって裏付けられています。ハルシネーション(捏造)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#57285, #57269)や、コード内のクラス名・メソッド名などの固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRの主題(リローダークリア時のフック無効化)とその影響(フォークプロセスでの問題解決)を的確に要約しており、PR内容と一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容は、PRおよびその中で言及されている関連PRの情報に限定されており、根拠のない外部知識(バージョンサポート状況、リリース日など)は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

PR間の前後関係(#57269のフォローアップであることなど)が正確に記述されており、時間表現の歪曲はありません。