Spring クライアントがサーバ状態を把握し再試行するよう改善

rails/spring

Spring クライアントは、サーバが起動中もしくは残存した pidfile が無効な状態でも、単なる接続エラーとして失敗していました。本 PR はその2ケースを検知し、boot timeout 相当の待機とサーバ再起動を自動的に行うことで、回復可能なシナリオを自動化します。

背景

Spring クライアントは、サーバに接続しバージョンハンドシェイクを行う際に、サーバがまだアプリケーションのプリロード中であるか、前回のサーバプロセスが死んで pidfile が残っていると Error connecting to Spring server を即座に返していました。これら 2 つの状態はいずれも一時的な問題であり、本来は待機または再起動で解決できるはずです。結果として、ユーザーは不可避なエラーに直面し、手動でサーバを再起動する必要がありました。

この問題を解決するためには、クライアント側でサーバの実際の状態を判断し、適切なリトライや再起動処理を行うロジックが必要です。さらに、リトライは無限ループにならないよう boot timeout を上限として制御することが求められました。

技術的な変更

lib/spring/client/run.rbServerReadTimeout 例外クラスを新設し、read_server_line メソッドにタイムアウト引数を導入しました。メソッドは IO.selectnil を返した場合に例外を送出し、呼び出し側で細かくハンドリングできるようしています。

verify_server_version の実装を大幅に改変し、ServerReadTimeout を捕捉した際に waiting_for_server_boot? が真であれば Spring.boot_timeout を使用して再度 read_server_line を試行します。再試行でもタイムアウトが発生すれば reboot_or_raise_connection_error を呼び出し、サーバが起動中でない場合は再起動ロジックへ移行します。

サーバから受信した行が nil だったケースでも同様に reboot_or_raise_connection_error が実行され、サーバが応答しない状態を一元的に処理します。これにより、従来の Error connecting to Spring server が発生する条件が減少し、回復可能なシナリオが自動化されます。

新たに追加された read_server_line(timeout = Spring.connect_timeout) メソッドは、デフォルトで従来の接続タイムアウトを使用しつつ、呼び出し側が明示的に Spring.boot_timeout を指定できるようになっています。コードブロックは以下の通りです。

def read_server_line(timeout = Spring.connect_timeout)
  raise ServerReadTimeout if IO.select([server], [], [], timeout).nil?
  server.gets
end

テスト側では test/support/acceptance_test.rb に 2 つの受け入れテストを追加し、1 クライアントがサーバ起動中に待機するケースと、古いサーバが死んでいるために再起動が必要になるケースを実証しています。テストは Spring.connect_timeoutSpring.boot_timeout を短縮設定し、BootWaitProbe モジュールで read_server_line の呼び出しをフックして待機経路を検証しています。

module BootWaitProbe
  def read_server_line(timeout = Spring.connect_timeout)
    if timeout == Spring.boot_timeout && ENV["EXPECT_BOOT_WAIT_PROBE"] == "1"
      File.write("#{boot_wait_path}", "1")
    end
    super
  end
end

Spring::Client::Run.prepend(BootWaitProbe)

設計判断

今回の実装は 新しい設定項目を導入せず、既存の Spring.connect_timeoutSpring.boot_timeout を組み合わせてリトライ時間を制御する方針を取りました。これにより、ユーザーが追加設定を覚える必要がなく、既存ドキュメントとの整合性が保たれます。

エラー処理は reboot_or_raise_connection_error に集約し、サーバが起動中か死んでいるかを判定した上で再起動または例外送出を行うことで、ロジックの重複を防ぎました。waiting_for_server_boot?server_booted? の判定結果に依存するため、将来的な判定ロジック変更にも一箇所だけ修正すれば済む設計になっています。

受け入れテストの追加は、実際の Spring プロセスとクライアント間のタイミングを再現し、boot‑wait パス再起動パス の両方が期待通りに動作することを保証します。テストは Shellwords 経由でプロセス管理し、ログとステータスファイルで結果を検証しています。

まとめ

本 PR は Spring クライアントにサーバ状態の認識と限定的リトライを導入し、起動待機中や古いサーバが残っているケースを自動回復させます。既存のタイムアウト設定を活用しつつ、エラーハンドリングを集中化した設計により、ユーザーへの影響を最小限に抑えつつ信頼性を向上させました。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文・背景・技術的な変更・設計判断・まとめの5セクションが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きコードブロックは正しい形式(```ruby:パス)で記述され、PRリンクも # を含むリンク化が適切です。

対象読者への適合性 ✓ PASS

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

内容は Spring の内部実装に関するエンジニア向けで、初心者向けの過剰な説明はありません。

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

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

各セクションは総論・各論・結論の段落構成になり、トピックセンテンスが先頭にあり、1段落1トピック・6文未満で空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは Diff に記載された変更と一致しており、ファイル名・内容・追加箇所が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

ServerReadTimeout、Spring.boot_timeout などの用語は PR と同一で誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な説明は PR の趣旨(サーバ起動待機と pidfile 死亡時の再起動)と合致し、根拠が示されています。

事実の突合 ✓ PASS

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

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

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

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

数値や固有名詞(PR番号#756、ファイルパス等)は正確です。

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

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

記事タイトルは PR "Make client smarter about Spring server state" の内容を適切に日本語化しています。

外部知識の正確性 ✓ PASS

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

LTSやリリース日程等の外部情報は一切含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現は PR の "boot timeout" という概念に忠実で、歪曲はありません。