フェーズドリスタート時にstaleなワーカー0からforkしてしまうバグを修正
fork_workerモードでフェーズドリスタートを行う際、ワーカー0が事前に再起動されていると新しいワーカーがstaleなワーカー0からforkされ続ける問題を修正しました。変更は@workers配列への挿入方式を変えるだけの1行修正です。
背景
fork_workerモードでは、ワーカー0がマスタープロセスの代わりにforkの親となり、他の全ワーカーはワーカー0からforkされます。フェーズドリスタートはコードを新しいものに切り替える手段ですが、ワーカー0が事前に終了・再起動されているケースでは意図通りに動作しない問題がありました。
フェーズドリスタート中、Pumaは @workers配列 の順序でワーカーを順番に再起動します。しかし新しく生成されたワーカーは配列の末尾に追記(<<)されるため、ワーカー0が事前に再起動されると、その新しいワーカー0は配列の末尾に追加されます。再起動の処理は配列の先頭から始まるため、「旧ワーカー0」が配列上は末尾に来ており、最後に再起動されるまで新しいワーカーのfork元として使われ続けます。
具体的な再現手順は以下の通りです:
-
fork_workerを有効にしてPumaを4ワーカーで起動 - ワーカー0を手動でKILL
- Pumaがワーカー0を自動再生成(配列末尾に追加)
- フェーズドリスタート(
USR1シグナル)を送信
この状態でフェーズドリスタートを実行すると、最初の3ワーカーはstaleな旧ワーカー0からforkされ、最後にのみ新しいワーカー0が起動します。結果として、最後のワーカー以外は古いコードを実行し続けるという深刻な問題が発生します。
技術的な変更
修正は lib/puma/cluster.rb の spawn_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を本番運用している環境では特に重要な修正です。