notify_safely のロック競合を解消し、ワーカー停止ハングを防止

puma/puma

notify_safely が内部ロックと競合し、Puma のクラスターモードでワーカー停止時にプロセスが無限にハングする問題を、IO#syswrite へ置き換えることで根本的に解決した変更を解説します。

背景

Puma のワーカープロセスは、Cluster::Worker#run の終了時に Server#stop を呼び出し、内部で notify_safely@notify << message により STOP_COMMAND を送信します。この書き込みは Ruby のバッファード I/O 経路を通り、IO#close が取得しようとする内部書き込みロックと競合することがあります。競合が発生すると、IO#close がロック待ちで sleep_forever に陥り、server_thread.join が永久に待ち続け、結果としてクラスターマスターは stop_workers でハングし、外部オーケストレータから SIGKILL される事態になります。

技術的な変更

lib/puma/server.rbnotify_safely 実装を、従来の @notify << message から @notify.syswrite(message) へ置き換えました。syswrite は単一の write(2) システムコールであり、MRI のバッファード I/O ロックをバイパスするため、IO#close とのデッドロックが起きません。また、syswrite は非同期シグナルセーフであるため、シグナルハンドラ(TERM/USR2/INT/HUP)から安全に呼び出すことができます。

@@
-      @notify << message
+      notify = @notify
+      notify.syswrite(message) if notify && !notify.closed?

さらに例外ハンドリングを拡張し、MRI 3.3 以降で Puma::IS_MRI && RUBY_VERSION > '3.3' の条件下では @log_writer.unknown_error に渡すようにしました。これは IOError, NoMethodError, Errno::EPIPE, Errno::EBADF などの例外をロギングしつつ、既存の挙動を保持します。

同時に、test/test_puma_server_notify_close_race.rb を追加し、2 つの回帰テストを実装しました。1 つ目はシグナルハンドラから notify_safely を呼び出しても ThreadError が発生しないことを確認し、2 つ目は多数の同時書き込みと停止操作を繰り返してもハングしないことを検証します。

設計判断

ロック競合を回避する手段として Mutex を導入する案も検討されましたが、Puma のシグナルハンドラは「トラップコンテキスト」から呼び出されるため、Mutex の synchronize"can't be called from trap context" という例外を引き起こします。そのため、非ロックでかつシグナルセーフな IO#syswrite が採用されました。syswrite は低レベルのシステムコールであるため、既存の API 互換性を保ちつつ、内部ロックの取得を回避できます。

変更は 後方互換性 を損なわず、従来通り @notify << message(真偽値 true/false での使用)からも動作します。syswrite の呼び出しは notify が nil もしくはクローズされている場合にスキップされるため、既存コードへの影響はありません。

まとめ

この PR は、notify_safely の実装を IO#syswrite に置き換えることで、Puma のワーカーハンドリング時に発生していたロック競合と無限ハングを根本的に解消しました。シグナルハンドラから安全に呼び出せる設計を維持しつつ、追加テストで回帰を防止することで、クラスターモード運用の信頼性が大幅に向上します。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
d375ef92

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文・背景・技術的な変更・設計判断(任意)・まとめの5要素が揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しい `````ruby:lib/puma/server.rb````` 形式ですが、PRリンクのテキストが "PR #3940" となっており、要求される `[##](URL)` 形式と完全に一致していません。リンク自体は機能しますが形式違反として警告を出します。

対象読者への適合性 ✓ PASS

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

エンジニア向けの専門的な記述で、初心者向けの過度な説明はありません。

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

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

各セクションは総論・各論・結論の構成を保ち、段落はトピックセンテンスで始まり1段落1トピックになっています。段落長も適切で空行で区切られています。

Diff内容との照合 ⚠ WARNING

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

コードブロックは Diff の一部(syswrite への置き換え)を正しく示していますが、例外ハンドリングの追加部分(rescue で @log_writer.unknown_error を呼ぶ)を省略しています。情報が不足しているため完全な照合とは言えず警告とします。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語は PR 内容と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な説明は PR の記述と合致しており、因果関係も正しく示されています。

事実の突合 ✓ PASS

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

記事の全ての主張は PR のタイトル・説明・Diff に裏付けられており、ハルシネーションはありません。

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

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

PR 番号 (#3940) やファイルパス、コード行の変更は正確です。数値の誤りは見当たりません。

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

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

記事タイトルは PR の内容(notify_safely のロック競合解消とワーカー停止ハング防止)を正確に表現しています。

外部知識の正確性 ✓ PASS

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

外部知識(LTS、リリース日等)への言及はなく、PR 情報に基づいた記述のみです。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記述と一致しています。