クライアント切断時のプロセスクラッシュを修正(Server・Application)

rails/spring

クライアントがハンドシェイク途中で切断した際に Errno::EPIPE が伝播してSpringプロセスが強制終了するバグが修正されました。Spring::Server#serveSpring::Application#serve の計5箇所の書き込みサイトが適切にレスキューされ、切断済みクライアントへの書き込み失敗がプロセス終了に繋がらなくなりました。

背景

Issue #724 として報告されたこのバグは、クライアントが Spring.connect_timeout(デフォルト5秒)でタイムアウトするなど、ハンドシェイク途中で切断した場合に顕在化します。Shopifyのモノレポのような起動が遅い環境では、プリローダーの起動待ちでタイムアウトが発生しやすく、再現性が高い問題でした。

クライアントが切断した後にサーバー側がそのソケットへ書き込もうとすると、OSは Errno::EPIPE(Broken pipe)を発生させます。この例外がレスキューされずに伝播すると、それを保持しているプロセスが終了します。影響を受けるクラッシュサイトは2つのプロセスにまたがる計5箇所でした。

以前にも同様の修正を試みた #725 がありましたが、Spring::Application#serve 内の4箇所のうち一部が見落とされていたため、本PRで改めて全箇所を網羅する形で修正が行われました。

技術的な変更

本PRの核心は ignore_client_disconnect という小さなヘルパーメソッドの導入です。Errno::EPIPE を飲み込む共通の抽象が用意されたことで、各クラッシュサイトを均一な方法でガードできるようになりました。

# 変更後
def ignore_client_disconnect
  yield
rescue Errno::EPIPE
end

このヘルパーは lib/spring/application.rbprivate セクションに追加され、serve メソッド内の4箇所に適用されています。変更前後の対比は以下の通りです。

変更前:

rescue Exception
  log "preload failed"
  client.puts(1) # preload failure
  raise
end

変更後:

rescue Exception
  log "preload failed"
  ignore_client_disconnect { client.puts(1) } # preload failure
  raise
end

既存の rescue Exception ハンドラ内部にある3つの書き込みは、同一スコープの rescue では捕捉できないため個別にラップする必要がありました。具体的には print_exceptionstreams.each(&:close)client.puts(1) if pid の3箇所です。

# 変更前
if streams && !e.is_a?(SystemExit)
  print_exception(stderr, e)
  streams.each(&:close)
end
client.puts(1) if pid

# 変更後
if streams && !e.is_a?(SystemExit)
  ignore_client_disconnect { print_exception(stderr, e) }
  streams.each { |stream| ignore_client_disconnect { stream.close } }
end
ignore_client_disconnect { client.puts(1) if pid }

Spring::Server#serve 側は既存の rescue SocketErrorrescue Errno::EPIPE を1ブロック追加するだけのシンプルな修正です。

# 変更後
rescue SocketError => e
  raise e unless client.eof?
rescue Errno::EPIPE => e
  log "client disconnected with error #{e.message}, ignoring command"

さらに、Errno::EPIPE の場合はログメッセージを "exception: #{e}" ではなく "client disconnected (#{e.message}), ignoring command" に変更することで、既知の切断イベントと予期しない例外を明確に区別できるようになりました。

テストは test/unit/application_test.rbtest/unit/server_test.rb として新規追加されました。UNIXSocket.pair でソケットペアを作成し、クライアント側を事前にクローズしてから serve を呼び出すことで、各クラッシュサイトを個別に検証するリグレッションテストになっています。

設計判断

ignore_client_disconnect を独立したヘルパーに切り出す設計 が採用されたことが重要なポイントです。begin/rescue Errno::EPIPE をインラインで書くことも可能でしたが、ヘルパーに集約することで「クライアント切断への耐性」という意図が各呼び出し箇所で明示的に伝わります。

rescue Exception ブロック内での Errno::EPIPE は、同レベルの別 rescue 節では捕捉できません。Rubyの例外処理の仕様上、同一 begin/end 内の rescue 節は上から順に評価され、先にマッチした節がすべての処理を担うためです。この制約に対応するため、問題のある書き込みをブロックごとラップするアプローチが採られました。

PRのコメントでは Errno::ECONNRESET も合わせてレスキューする案が検討されましたが、Issue #724 で実際に報告された症状(Errno::EPIPE)に修正範囲を絞り込み、過度な変更を避ける方針が選択されています。application.rb:164SocketError に関するクラッシュサイトも別経路として存在しますが、スコープ外として今後のフォローアップに委ねられています。

まとめ

今回の修正は、ignore_client_disconnect という単一のヘルパーを導入することで、2つのプロセスにまたがる5箇所のクラッシュサイトを一貫した方法でガードするものです。特に Spring::Server のacceptループがクラッシュして以降のすべてのクライアントがタイムアウトするという最悪ケースが解消され、spring stop を手動実行しなければ復旧できない状況が回避されるようになりました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
c1edb0b7

この記事は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リンク記法の正確性

ファイル名付きシンタックスハイライト、PR/Issue番号のリンク化など、すべてのカスタムMarkdown構文が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Errno::EPIPE、ハンドシェイク、ソケットなどの技術用語を前提としており、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクションが総論→各論の構成で、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が非常によく守られており、読みやすいです。

Diff内容との照合 ⚠ WARNING

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

コードの引用は概ね正確ですが、「技術的な変更」セクションの末尾で、application.rbのログメッセージの説明の直後にserver.rbのコードブロックを引用しており、読者に若干の混乱を招く可能性があります。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Errno::EPIPE, acceptループ, プリローダーなど、文脈に応じた技術用語が正確に使用されています。

説明の技術的正確性 ✓ PASS

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

クライアント切断時にEPIPEが発生するメカニズムや、同一rescue節で例外を捕捉できないRubyの仕様など、技術的な説明は正確かつ論理的です。

事実の突合 ✓ PASS

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

Shopifyのモノレポでの再現性、影響を受ける5箇所のクラッシュサイト、過去のPR(#725)への言及など、記事内のすべての主張がPR Descriptionで裏付けられており、ハルシネーションは見られません。

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

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

PR番号(#751)、Issue番号(#724)、クラッシュサイトの数(5箇所)など、記事内のすべての数値・固有名詞は正確です。

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

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

記事のタイトルはPR「Don't crash on client disconnect (Server and Application)」の内容を的確に和訳・要約しており、主題と一致しています。

外部知識の正確性 ✓ PASS

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

記事の内容はすべてPR情報に基づいており、バージョンサポート状況やリリース日程といったPR外の知識の追記はありません。

時間表現の正確性 ✓ PASS

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

「以前にも同様の修正を試みた」「今後のフォローアップに委ねられています」といった時間表現は、PR内の「closed」「worth a follow-up」といった記述と正確に一致しています。