Coverage「suspended」状態でのISeqダンプエラーを修正

rails/bootsnap

Ruby 4.0.4以降でCoverageが「suspended」状態のときにISeqのキャッシュが失敗する問題を修正しました。Coverage.running?では「停止中」と「中断中」を区別できないため、Coverage.stateを使った判定に切り替えています。

背景

Ruby 4.0.4へのアップグレード後、テストスイートの起動時に "should not compile with coverage" という RuntimeError が発生する問題が報告されました(#547)。この問題はRuby 4.0.3では発生せず、Coverageの収集を意図していない環境でも再現しました。

PRの説明によると、これはRuby 4.0.4でISeqダンプ時のCoverage状態チェックが実効化されたことが原因です。それ以前のRubyではこのチェック自体が機能していなかったため、問題が表面化していませんでした。RSpecなどのテストフレームワークはカバレッジ収集を一時的に Coverage.suspend で中断する場合があります。この「suspended」状態では Coverage.running?true を返すため、Bootsnapは誤ってISeqキャッシュの利用をスキップしていました。

結果として、カバレッジを収集していないつもりのユーザーがBootsnap起動時にエラーに直面するという、回避が難しい状況が生まれていました。

技術的な変更

Coverage.running?Coverage.state による判定に置き換えることで、「suspended」と「idle」を区別できるようにしました。

変更前:

def load_iseq(path)
  # Having coverage enabled prevents iseq dumping/loading.
  return nil if defined?(Coverage) && Coverage.running?

  Bootsnap::CompileCache::ISeq.fetch(path.to_s)
end

変更後:

def load_iseq(path)
  # Having coverage enabled prevents iseq dumping/loading.
  return nil if coverage_on?

  Bootsnap::CompileCache::ISeq.fetch(path.to_s)
end

判定ロジックは coverage_on? メソッドとして切り出され、Rubyバージョンに応じて実装が切り替わります。Coverage.state は Ruby 3.1 で導入されたAPIのため、それ以前のバージョンとの互換性を保つための分岐が必要でした。

if RUBY_VERSION < "3.1."
  def coverage_on?
    defined?(Coverage) && Coverage.running?
  end
else
  def coverage_on?
    defined?(Coverage) && Coverage.state != :idle
  end
end

Ruby 3.1以降では Coverage.state:idle(停止)、:suspended(中断)、:running(実行中)のいずれかを返します。:idle のみをキャッシュ利用可能な状態とみなすことで、「中断中」でも正しくISeqキャッシュが機能するようになります。

併せて、カバレッジ状態をパラメータ化した統合テスト test/integration/app_boot_test.rb が追加されました。COVERAGE 環境変数に started / suspended / stopped を渡してフィクスチャアプリを起動し、各状態での動作を検証します。test_boot_with_coverage_suspended は Ruby 3.1以降・MRI限定でのみ実行されるようスキップ条件が設定されています。

設計判断

バージョン分岐をクラス定義時に解決するアプローチが採られています。load_iseq の呼び出しパスに条件分岐を埋め込む代わりに、coverage_on? メソッドをRubyバージョンに応じて静的に定義することで、実行時の判定コストをゼロに抑えています。これはBootsnap自身がロードパフォーマンスに敏感なライブラリであることを踏まえた判断といえます。

なお、test_boot_frozen_string_literal_and_coverageskip("Need to find a workaround for this...") として残されており、frozen string literalコンパイルとカバレッジ収集の組み合わせについては別途対応が必要な課題として明示されています。

まとめ

本PRは、RubyのCoverage APIの細かい状態遷移—「stopped」「suspended」「running」の違い—をBootsnap側で正しく扱えるようにした修正です。API差異をメソッドの静的な実装切り替えで吸収する手法は、ホットパスのオーバーヘッドを避けながら複数Rubyバージョンへの対応を維持する実践的なパターンとして参考になります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
94013a80

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)→背景・技術詳細(各論)→まとめ(結論)」という理想的な3部構成が明確に守られています。各セクションの役割も適切です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト、Issue番号とPR番号のリンク記法など、カスタムMarkdown構文がすべて正しく使用されています。

対象読者への適合性 ✓ PASS

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

BootsnapやRubyのCoverage APIに関する知識を前提としており、専門知識を持つエンジニアという対象読者に適合した内容です。

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

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

各セクション、各パラグラフが「総論→各論」の構造で書かれており、トピックセンテンスも明確です。1段落1トピックの原則が守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコード(変更前・変更後)、ファイルパス、テストの追加に関する説明は、すべて提供されたDiff情報と正確に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「ISeq」「Coverage」「suspended」「idle」など、関連する技術用語が文脈に応じて正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

「`Coverage.running?`では中断状態を区別できない」という問題の核心や、`Coverage.state`を用いた解決策の説明が、技術的に正確かつ論理的です。

事実の突合 ⚠ WARNING

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

「設計判断」セクションの「実行時の判定コストをゼロに」という記述は、PRに明記されていないものの、コード構造から妥当に推測できる内容です。しかし、厳密にはPR情報外の推測にあたります。

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

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

PR番号(#549)、Issue番号(#547)、Rubyのバージョン番号(4.0.4, 3.1など)がすべて正確に記載されています。

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

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

PRの主題である「Coverageがsuspended状態の扱い」を的確に捉え、「ISeqダンプエラーを修正」という具体的な影響にまで言及しており、秀逸なタイトルです。

外部知識の正確性 ⚠ WARNING

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

「Coverage.stateが:idle, :suspended, :runningを返す」という説明は、コードの理解に不可欠ですが、PR情報には記載のない外部知識(RubyのAPI仕様)です。

時間表現の正確性 ✓ PASS

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

PR Descriptionの「starting from Ruby 4.0.4 as until then...」という時間的な前後関係を、「Ruby 4.0.4へのアップグレード後」「それ以前のRubyでは」と正確に表現できています。