並列テスト終了後に残存するDRbスレッドのリークを修正

rails/rails

プロセスベースの並列テスト実行後、親プロセスが DRb.stop_service を呼び出していなかったことで DRb の accept ループスレッドが残存し、一部の環境でテストプロセスが終了できなくなる問題を修正しました。

背景

ActiveSupport::Testing::Parallelization は、初期化時に DRb.start_service でDRbサービスを起動し、ワーカープロセスへの作業キューをDRb経由で提供します。しかし、#shutdown メソッドはキューサーバのシャットダウンとワーカープロセスの回収のみを行い、DRb.stop_service を呼び出していませんでした。

この不整合により、DRbが内部で管理する accept ループスレッドaccept_or_shutdown / main_loop)がワーカー終了後も親プロセス内で生き続けました。ワーカープロセス側は fork 後に DRb.stop_service を呼び出していたため、クリーンアップが欠けていたのは親プロセスのみです。CIで 0 failures を出力した後にプロセスがタイムアウトするという形で顕在化したのも、この非デーモンスレッドが Rubyランタイムの終了をブロックしていたためです。

技術的な変更

Parallelization#shutdown の末尾、すべてのワーカープロセスが Process.waitpid で回収された後に DRb.stop_service の呼び出しを追加しました。

変更前:

def shutdown
  # ...
  @queue_server.shutdown
  @worker_pool.each { Process.waitpid(_1) ... }

  Parallelization.run_cleanup_hooks.each(&:call)
end

変更後:

def shutdown
  # ...
  @queue_server.shutdown
  @worker_pool.each { Process.waitpid(_1) ... }

  DRb.stop_service

  Parallelization.run_cleanup_hooks.each(&:call)
end

変更箇所は2行の追加のみです。DRb.stop_service はクリーンアップフックの直前、つまりすべてのワーカー回収後に配置されており、DRbサービスが確実に不要になったタイミングで停止されます。

あわせて、リグレッションテストが activesupport/test/parallelization_test.rb に追加されました。テストは shutdown 後に Thread.list を検査し、バックトレースに drb/drb.rb を含む alive なスレッドが存在しないことを assert_empty で検証します。Process.respond_to?(:fork) が偽の環境(Windowsなど)ではスキップされます。

test "shutdown stops the DRb service started in initialize" do
  skip "Process-based parallelization requires fork" unless Process.respond_to?(:fork)

  parallelization = ActiveSupport::Testing::Parallelization.new(1)
  parallelization.start
  parallelization.shutdown

  leaked = Thread.list
    .reject { |t| t == Thread.main }
    .select(&:alive?)
    .select do |t|
      bt = Array(t.backtrace).join("\n")
      bt.include?("drb/drb.rb") && bt.match?(/accept_or_shutdown|main_loop/)
    end

  assert_empty leaked,
    "Expected Parallelization#shutdown to call DRb.stop_service. Leaked:\n" +
    leaked.map { |t| Array(t.backtrace).first(5).join("\n  ") }.join("\n---\n")
end

設計判断

DRb.stop_service の挿入位置は、ワーカー回収後・クリーンアップフック前という順序が意図的に選ばれています。ワーカーが生きている間はDRbを介したキューアクセスが発生しうるため、Process.waitpid による全ワーカー回収を完了させてから停止するのが安全です。一方、クリーンアップフックは独立した後処理であり、DRbサービスへの依存がないため、その前に停止しても問題ありません。

変更の規模は最小限に抑えられており、start / shutdown の対称性(サービスを起動したものが停止する責任を持つ)という原則を取り戻す修正です。ワーカー側がすでに DRb.stop_service を呼び出していたという事実は、この呼び出しが必要であることを設計上も認識していたことを示しており、親プロセス側での漏れであったことが明確です。

まとめ

#shutdownDRb.stop_service を1行追加するだけで、#initialize で開始したDRbサービスのライフサイクルが対称的に閉じられるようになります。起動と停止の責任を同一クラス内で完結させるという原則を徹底することで、CI環境でのプロセスタイムアウトという形で顕在化していた問題が根本から解消されます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
ca47d3fc

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)、背景・技術的な変更・設計判断(各論)、まとめ(結論)という「総論→各論→結論」の構成が明確です。必須要素はすべて満たされており、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト(```ruby:ファイルパス)やPR番号のリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

DRb、スレッドリーク、プロセスフォークといった専門用語が適切に使われており、初心者向けの過度な説明がなく、対象読者である専門知識を持つエンジニアに適合しています。

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

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

各セクションが総論から各論へと展開され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に読みやすい構造になっています。

Diff内容との照合 ✓ PASS

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

記事内で引用されている`activesupport/lib/active_support/testing/parallelization.rb`と`activesupport/test/parallelization_test.rb`のコードは、提供されたDiffの内容と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「accept ループスレッド」「Process.waitpid」など、PRの文脈に沿った技術用語が正確に使用されています。

説明の技術的正確性 ✓ PASS

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

DRbサービスが停止されずにスレッドがリークし、テストプロセスが終了しなくなるという問題の因果関係が、技術的に正確かつ論理的に説明されています。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題の原因、影響、解決策)がPRのDescriptionによって裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#57427)がメタデータ部分で正しく参照されており、記事内容との整合性が取れています。

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

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

記事タイトル「並列テスト終了後に残存するDRbスレッドのリークを修正」は、PRの主題である「Stop DRb service when shutting down parallel test workers」を的確に要約しています。

外部知識の正確性 ✓ PASS

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

記事の内容は提供されたPR情報に完全に準拠しており、バージョンサポート状況やリリース日程といったPR外の知識の追加はありません。

時間表現の正確性 ✓ PASS

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

PR Descriptionにある「Worker processes already call...」という事実を「ワーカープロセス側は...呼び出していた」と正確な時間表現で記述しています。