フェーズドリスタート時にstaleなワーカー0からforkしてしまうバグを修正

puma/puma

fork_workerモードでフェーズドリスタートを行う際、ワーカー0が事前に再起動されていると新しいワーカーがstaleなワーカー0からforkされ続ける問題を修正しました。変更は@workers配列への挿入方式を変えるだけの1行修正です。

背景

fork_workerモードでは、ワーカー0がマスタープロセスの代わりにforkの親となり、他の全ワーカーはワーカー0からforkされます。フェーズドリスタートはコードを新しいものに切り替える手段ですが、ワーカー0が事前に終了・再起動されているケースでは意図通りに動作しない問題がありました。

フェーズドリスタート中、Pumaは @workers配列 の順序でワーカーを順番に再起動します。しかし新しく生成されたワーカーは配列の末尾に追記(<<)されるため、ワーカー0が事前に再起動されると、その新しいワーカー0は配列の末尾に追加されます。再起動の処理は配列の先頭から始まるため、「旧ワーカー0」が配列上は末尾に来ており、最後に再起動されるまで新しいワーカーのfork元として使われ続けます。

具体的な再現手順は以下の通りです:

  1. fork_workerを有効にしてPumaを4ワーカーで起動
  2. ワーカー0を手動でKILL
  3. Pumaがワーカー0を自動再生成(配列末尾に追加)
  4. フェーズドリスタート(USR1シグナル)を送信

この状態でフェーズドリスタートを実行すると、最初の3ワーカーはstaleな旧ワーカー0からforkされ、最後にのみ新しいワーカー0が起動します。結果として、最後のワーカー以外は古いコードを実行し続けるという深刻な問題が発生します。

技術的な変更

修正は lib/puma/cluster.rbspawn_workersメソッド 内、わずか1行の変更です。

変更前:

@workers << WorkerHandle.new(idx, pid, @phase, @options)

変更後:

@workers.insert(idx, WorkerHandle.new(idx, pid, @phase, @options))

<<(末尾追記)から Array#insert(idx, ...)(インデックス指定挿入)に変更することで、新しく生成されたワーカーが常に正しいインデックス位置(idx)に挿入されるようになります。ワーカー0が再生成された場合も@workers[0]に挿入されるため、配列の順序がワーカーのインデックスと常に一致します。

PRの説明によると、当初は@workers配列をソートする方法も検討されましたが、インデックス指定での挿入の方がシンプルであるとして現在の実装が採用されました。

追加されたテスト test_phased_restart_with_fork_worker_worker_order は、この修正を検証します。ワーカー0を意図的にTERMシグナルで終了させ、再生成後にフェーズドリスタートを実行し、フェーズ1のワーカーがインデックス0から順に起動することを確認します。

phase_1_indexes = @server_log.scan(/Worker (\d+) \(PID: \d+\) booted in [.0-9]+s, phase: 1/).flatten.map(&:to_i)
assert_equal 0, phase_1_indexes.first

設計判断

@workers配列のインデックスとワーカーインデックス(idx)の整合性を保つ ことが今回の修正の核心です。

fork_workerモードでは、フェーズドリスタートの再起動順序がワーカーのfork元に直接影響します。@workers配列の順序がワーカーのインデックスと一致していれば、ワーカー0は必ず最初に再起動され、後続のワーカーは新しいワーカー0からforkされます。逆に配列の順序が崩れると、古いワーカー0を起点にforkが連鎖し、コードの切り替えが意図通りに行われません。

insertへの変更は既存の配列操作インターフェースを変えるだけであり、APIの変更もなく、影響範囲は最小限です。ワーカーが正常に起動している通常ケースではidxと配列の末尾位置が一致するため、動作は変わりません。この修正が効果を発揮するのは、ワーカー0が事前に異常終了・再起動されたという例外的なケースに限られます。

まとめ

fork_workerモードにおけるフェーズドリスタートの信頼性は、@workers配列の順序とワーカーインデックスの整合性に依存しています。今回の修正は1行の変更でその整合性を保証し、ワーカー0の異常終了後でも全ワーカーが正しく新しいコードへ切り替わることを担保します。fork_workerを本番運用している環境では特に重要な修正です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
322ca866

この記事は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:filepath)とPR番号のリンク記法([#123](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Pumaの`fork_worker`モードやフェーズドリスタートに関する専門的な内容であり、対象読者であるエンジニアに適した技術レベルと表現で記述されています。

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

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

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切で、可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されている`lib/puma/cluster.rb`および`test/test_integration_cluster.rb`のコードは、提供されたDiffの内容と完全に一致しており、正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「フェーズドリスタート」「staleなワーカー」「fork_worker」といった技術用語がPR情報と一致しており、文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

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

問題の原因(`@workers`配列への末尾追記)から解決策(`insert`によるインデックス指定挿入)までの技術的な説明は論理的かつ正確で、PRの内容とも整合しています。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題の再現手順、`sort`の検討など)は、PRのDescriptionやDiffの内容によって裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#3853)やファイルパス、メソッド名などの固有名詞はすべて正確に記載されています。

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

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

記事のタイトル「フェーズドリスタート時にstaleなワーカー0からforkしてしまうバグを修正」は、PRのタイトルと内容を正確に反映しています。

外部知識の正確性 ✓ PASS

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

記事にはPR情報に基づかない外部知識(バージョンサポート状況、リリース日程など)は含まれておらず、事実に基づいた記述が徹底されています。

時間表現の正確性 ✓ PASS

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

「事前に」「再起動後」などの時間表現は、PRで説明されている事象の前後関係と正確に一致しており、誤解を招く表現はありません。