Coverage「suspended」状態でのISeqダンプエラーを修正
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_coverage は skip("Need to find a workaround for this...") として残されており、frozen string literalコンパイルとカバレッジ収集の組み合わせについては別途対応が必要な課題として明示されています。
まとめ
本PRは、RubyのCoverage APIの細かい状態遷移—「stopped」「suspended」「running」の違い—をBootsnap側で正しく扱えるようにした修正です。API差異をメソッドの静的な実装切り替えで吸収する手法は、ホットパスのオーバーヘッドを避けながら複数Rubyバージョンへの対応を維持する実践的なパターンとして参考になります。