`shutdown_debug` に `on_force` オプションを追加し、強制シャットダウン時のみバックトレースを出力可能に

puma/puma

Pumaのグレースフルシャットダウン時のデバッグ機能 shutdown_debugon_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.rbforce_shutdown_after のデフォルト値が -1(無制限待機)として明示的に定義されました。ThreadPool#shutdown のシグネチャも shutdown(timeout=-1) から shutdown(timeout) に変更され、デフォルト引数が廃止されています。

force_shutdown_after のデフォルト値の明示:

force_shutdown_after: -1,

timeout == -1 のとき threads.each(&:join) でタイムアウトなしに待機する動作は変わりませんが、呼び出し側に -1 を渡す責務を持たせることで、ServerThreadPool の責務境界が整理されています。

設計判断

バックトレースダンプの責務を 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 は、valfalse または 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_debugon_force オプションは、単なる機能追加にとどまらず、シャットダウンデバッグの責務を Server から ThreadPool へ移管する設計の整理を伴っています。force_shutdown_after のタイムアウト後に残存するスレッドのバックトレースのみを出力できるようになったことで、本当にシャットダウンをブロックしているスレッドの特定が容易になりました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
1d038a83

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)」「背景・技術的な変更・設計判断(各論)」「まとめ(結論)」という3部構成が明確に守られています。各セクションの役割がはっきりしており、非常に理解しやすい構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト(```言語:ファイルパス)や、PR・Issue番号のリンク記法([#123](URL))がガイドライン通りに正しく使用されています。

対象読者への適合性 ✓ PASS

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

Pumaの内部構造に関する知識を前提としており、専門知識を持つエンジニアという対象読者に適合した内容と技術レベルです。

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

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

各セクションが総論→各論で構成され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されています。1段落1トピックも守られており、可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードは、提供されたDiffの内容と完全に一致しています。変更点を説明するために必要な部分が的確に抽出されており、正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「グレースフルシャットダウン」「バックトレース」「ThreadPool」といった技術用語が、文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

「バックトレースダンプの責務をThreadPoolに集約した」といった説明は、Diffのコード変更から論理的に導かれるものであり、技術的に正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張(機能追加の目的、関連Issue、設計判断など)は、PRのタイトル、Description、Diffの内容によって裏付けられています。ハルシネーションは検出されませんでした。

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

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

PR番号(#3671)、Issue番号(#3666)などの数値や固有名詞はすべて正確に記載されています。

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

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

記事のタイトルはPRの主題「Add `on_force` option to `shutdown_debug`」を正確に反映し、その変更がもたらす価値(強制シャットダウン時のみバックトレースを出力)まで含んでおり、秀逸です。

外部知識の正確性 ✓ PASS

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

バージョンサポート状況やリリース日程など、PR情報に記載のない外部知識の持ち込みは見られませんでした。

時間表現の正確性 ✓ PASS

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

「これまでの...」といった過去の状態と今回の変更を対比する時間表現が正確に使用されており、誤解を招く表現はありません。