[rails/bootsnap] `require`装飾中に`Bootsnap.unload_cache!`が呼ばれた場合の安全性を向上

rails/bootsnap

Context

BootsnapはRubyのrequireをオーバーライドして、ロードパスのキャッシュを提供することでRailsアプリケーションの起動を高速化するライブラリです。しかし、requireされたファイル内でBootsnap.unload_cache!が呼ばれると、キャッシュインデックスがnilになり、その後のキャッシュ操作でエラーが発生する問題がありました。

この問題は、Railsをロードする前に多数のファイルをrequireする特定のエントリーポイントで実際に発生しており、実運用環境での影響が確認されていました。

Technical Detail

問題の発生箇所

Bootsnapのrequire装飾では、ファイルをrequireした後にキャッシュインデックスへの登録処理を行います。しかし、requireされたファイル内でBootsnap.unload_cache!が呼ばれると、Bootsnap::LoadPathCache.loaded_features_indexnilになり、その後のidentifyregisterメソッド呼び出しでNoMethodErrorが発生していました。

修正内容

lib/bootsnap/load_path_cache/core_ext/kernel_require.rbで、キャッシュインデックスへのアクセスに安全なナビゲーション演算子(&.)を使用するよう変更されました。

変更後(キャッシュミス時のパス):

if (cursor = Bootsnap::LoadPathCache.loaded_features_index.cursor(string_path))
  ret = require_without_bootsnap(path)

  # The file we required may have unloaded the cache
  resolved = Bootsnap::LoadPathCache.loaded_features_index&.identify(string_path, cursor)
  Bootsnap::LoadPathCache.loaded_features_index&.register(string_path, resolved)

  return ret
else
  return require_without_bootsnap(path)
end

変更後(キャッシュヒット時のパス):

else
  # Note that require registers to $LOADED_FEATURES while load does not.
  ret = require_without_bootsnap(resolved)

  # The file we required may have unloaded the cache
  Bootsnap::LoadPathCache.loaded_features_index&.register(string_path, resolved)
  return ret
end

テストの追加

2つのテストケースが追加され、キャッシュヒット時とキャッシュミス時の両方でBootsnap.unload_cache!が安全に処理されることが検証されています。

キャッシュミス時のテスト:

def test_unload_cache_from_require_on_cache_miss
  skip("Need a working Process.fork to test in isolation") unless Process.respond_to?(:fork)
  begin
    assert_nil LoadPathCache.load_path_cache
    cache = Tempfile.new("cache")
    pid = Process.fork do
      LoadPathCache.setup(cache_path: cache.path, development_mode: true, ignore_directories: nil)
      dir = File.realpath(Dir.mktmpdir)
      LoadPathCache.loaded_features_index.expects(:key?).returns(false)
      LoadPathCache.load_path_cache.expects(:find).returns(LoadPathCache::FALLBACK_SCAN)
      LoadPathCache.loaded_features_index.expects(:cursor).returns(12)

      $LOAD_PATH.push(dir)
      path = "#{dir}/a.rb"
      File.write(path, <<~RUBY)
        Bootsnap.unload_cache!
      RUBY

      require(path)
    end
    _, status = Process.wait2(pid)
    assert_predicate status, :success?
  ensure
    cache.close
    cache.unlink
  end
end

テストではProcess.forkを使用して隔離された環境でキャッシュのアンロードを検証しています。mochaを使ってキャッシュミスをシミュレートし、requireされたファイル内でBootsnap.unload_cache!が呼ばれてもプロセスが正常終了することを確認しています。

Impact

この修正により、requireされたファイル内でBootsnap.unload_cache!が呼ばれても、安全にキャッシュ操作をスキップできるようになりました。これは、特殊なエントリーポイントを持つアプリケーションや、動的にBootsnapの状態を制御する必要があるケースで重要な改善となります。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

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

品質レビュー結果

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

Review Criteria:

ガイドライン準拠 ✓ PASS

記事構成とDiffDaily Styleへの準拠状況

記事の構成、カスタムMarkdown構文、対象読者への適合性、すべてのガイドライン項目を完璧に満たしています。特に、コードブロック前後の空行やファイル名付きシンタックスハイライトが正しく使用されており、可読性が非常に高いです。

  • 記事構成(Title、Context、Technical Detail)
  • DiffDaily Styleガイド準拠
  • カスタムMarkdown活用
  • 対象読者への適合性
技術的整合性 ✓ PASS

技術的な正確性と表現の適切性

技術的な整合性は完璧です。Diff内容との照合において、引用されたコードは正確であり、ファイルパスも一致しています。安全なナビゲーション演算子(&.)の役割についての説明も技術的に正確かつ明瞭です。

  • 技術用語の正確性
  • コード例の正確性
  • 説明の技術的正確性
PR内容との整合性 ✓ PASS

元のPR情報との一致度

PRの内容との整合性も問題ありません。記事内のすべての主張はPRのDescriptionやDiffによって裏付けられており、ハルシネーションは検出されませんでした。PR番号やリポジトリ名などの固有名詞も正確です。

  • タイトル・説明の一致
  • Diff内容の正確な反映
  • 推測の排除