`shutdown_debug` に `on_force` オプションを追加し、強制シャットダウン時のみバックトレースを出力可能に
Pumaのグレースフルシャットダウン時のデバッグ機能 shutdown_debug に on_force: true オプションが追加され、タイムアウトによる強制シャットダウンが発生した場合にのみスレッドのバックトレースを出力できるようになりました。これにより、実際にハングしているスレッドを特定しやすくなります。
背景
これまでの shutdown_debug: true は、グレースフルシャットダウンの開始直後にすべてのスレッドのバックトレースをダンプしていました。このタイミングでは処理中のスレッドと待機中のスレッドが混在しており、どのスレッドが実際にシャットダウンをブロックしているかを判別するのが困難でした。
#3666 では、force_shutdown_after のタイムアウトが経過した後も残存しているスレッドのみをログ出力する機能が要望されていました。タイムアウト後に生き残っているスレッドこそが問題の原因であるため、そのタイミングでバックトレースを取得することがデバッグ上の価値が高いという指摘です。
技術的な変更
shutdown_debug の変更は DSL、ThreadPool、Server の3層にまたがっています。変更の核心は、バックトレースのダンプタイミングを「シャットダウン開始時」から「強制シャットダウン直前」へ移動させたことです。
lib/puma/dsl.rb の変更:
shutdown_debug メソッドに on_force: キーワード引数が追加されました。
変更前:
def shutdown_debug(val=true)
@options[:shutdown_debug] = val
end
変更後:
def shutdown_debug(val = true, on_force: false)
@options[:shutdown_debug] = val && on_force ? :on_force : val
end
on_force: true を指定すると @options[:shutdown_debug] に :on_force シンボルが格納されます。従来の true / false に加え、:on_force という第3の状態が導入された形です。
lib/puma/thread_pool.rb の変更:
@shutdown_debug オプションが ThreadPool のインスタンス変数として保持されるようになり、バックトレースのダンプロジックが graceful_shutdown から ThreadPool#shutdown に移動しました。
# shutdown_debug == true の場合: シャットダウン開始時にダンプ
if @shutdown_debug == true
shutdown_debug("Shutdown initiated")
end
# タイムアウト経過後、まだスレッドが残っていれば on_force 判定
join.call(timeout)
if @shutdown_debug == :on_force && !threads.empty?
shutdown_debug("Shutdown timeout exceeded")
end
shutdown_debug == true の場合はシャットダウン開始直後に全スレッドをダンプし、shutdown_debug == :on_force の場合は join.call(timeout) でタイムアウトを待った後にまだ残存しているスレッドがあれば初めてダンプします。
lib/puma/server.rb の変更:
graceful_shutdown からバックトレースダンプのロジックが完全に削除され、@thread_pool.shutdown の呼び出しも簡略化されました。
変更前:
def graceful_shutdown
if options[:shutdown_debug]
threads = Thread.list
# ... バックトレースダンプ処理 ...
end
if @thread_pool
if timeout = options[:force_shutdown_after]
@thread_pool.shutdown timeout.to_f
else
@thread_pool.shutdown
end
end
end
変更後:
def graceful_shutdown
if @status != :restart
@binder.close
end
@thread_pool.shutdown(options[:force_shutdown_after])
end
あわせて lib/puma/configuration.rb で force_shutdown_after のデフォルト値が -1(無制限待機)として明示的に定義されました。ThreadPool#shutdown のシグネチャも shutdown(timeout=-1) から shutdown(timeout) に変更され、デフォルト引数が廃止されています。
force_shutdown_after のデフォルト値の明示:
force_shutdown_after: -1,
timeout == -1 のとき threads.each(&:join) でタイムアウトなしに待機する動作は変わりませんが、呼び出し側に -1 を渡す責務を持たせることで、Server と ThreadPool の責務境界が整理されています。
設計判断
バックトレースダンプの責務を ThreadPool に集約する設計が採用されました。
変更前は Server#graceful_shutdown がスレッドのダンプを担い、タイムアウト処理を担う ThreadPool#shutdown とは別々に動作していました。この構造では「タイムアウト後に残存するスレッドだけをダンプする」という処理を Server 側から実装することが難しく、ThreadPool 内部にロジックを移動することが自然な選択です。
on_force の状態表現に新しいクラスを導入せず、:on_force シンボルを使ったことも注目点です。true / false / :on_force の3値を既存の @options ハッシュに収めることで、設定の伝搬経路を変えずに新機能を追加しています。また、shutdown_debug DSLメソッドの条件式 val && on_force ? :on_force : val は、val が false または nil の場合には on_force の指定に関わらず false / nil が格納されるため、デバッグ機能を無効化する既存の使い方に影響を与えません。
テストコードでは、shutdown_requests ヘルパーが test/test_puma_server.rb から独立した test/helpers/test_puma/shutdown_requests.rb モジュールに切り出され、新設の test/test_puma_server_shutdown_debug.rb でも共有されています。テストの再利用性を高める変更です。
まとめ
shutdown_debug の on_force オプションは、単なる機能追加にとどまらず、シャットダウンデバッグの責務を Server から ThreadPool へ移管する設計の整理を伴っています。force_shutdown_after のタイムアウト後に残存するスレッドのバックトレースのみを出力できるようになったことで、本当にシャットダウンをブロックしているスレッドの特定が容易になりました。