ActionCableの再起動時に接続レジストリをクリアしてメモリリークを防止

rails/rails

ActionCable の restart メソッドが接続を閉じた後に connections_map をクリアしないため、開発モードのリロード時に死んだコネクションが蓄積し、メモリリークと統計値の過大計測が発生していました。本 PR はそのレジストリクリア処理を追加し、リソースの一元的な解放を実現します。

背景

ActionCable::Server::Base#restart は全接続を閉じた後、@worker_pool@heartbeat_timer などをミューテックスで安全に停止しますが、connections_map からエントリを除去しない点が設計上の抜け穴でした。

この抜け穴は、Socket#handle_close が非同期で server.remove_connection を呼び出す仕組みと相まって顕在化します。restart がワーカープールを halt すると、キューに残った remove_connection コールバックが実行されず、閉じたコネクションオブジェクトが connections_map に残り続けます。

開発モードではコードリロードごとに ActionCable.server.restart が呼び出されるため、死んだコネクションが蓄積し #connectionsopen_connections_statistics が実際以上の数を報告し、最終的にメモリ使用量が増加します。これが本 PR が対象とした根本的な問題です。

技術的な変更

restart のミューテックス保護ブロック内に connections_map.clear を挿入し、他リソースと同時にレジストリをリセットする実装へと拡張しました。

@@ -76,6 +76,10 @@ def restart
         end

         @mutex.synchronize do
+          # Drop the closed connections. remove_connection normally runs on the
+          # worker pool, which we halt below, so the entries would otherwise leak.
+          connections_map.clear
+
           # Shutdown the heartbeat timer
           @heartbeat_timer.shutdown if @heartbeat_timer
           @heartbeat_timer = nil

テストスイートにも #restart clears the connections registry という検証ケースを追加し、restart 後に @server.connections が空であることを明示的に確認しています。

@@
   class ClosableConnection
     def close(*); end
   end

   test "#restart clears the connections registry" do
     @server.add_connection(ClosableConnection.new)
     assert_equal 1, @server.connections.size

     @server.restart

     # remove_connection normally runs via the worker pool, which restart halts,
     # so the closed connections must be dropped here or they leak (e.g. on every
     # dev-mode code reload, which calls restart on the singleton server).
     assert_empty @server.connections
   end

connections_map.clear は冪等であるため、remove_connectionrestart 前に走っていた場合でも競合は発生せず、安全にレジストリをリセットできます。このシンプルなクリア処理により、追加の同期ロジックや複雑な状態管理は不要となります。

設計判断

本変更は、@mutex で保護された既存のリソース解放フローに connections_map.clear を組み込むという、一貫した設計パターンを踏襲しています。直前にマージされた PR #57700 で @heartbeat_timer の停止が同様に追加されたことと同様です。

clear を選択した背景は、レジストリの状態をリセットするだけで十分であり、個々のエントリを削除するよりもコードが簡潔でありながら安全性が確保できる点です。clear は idempotent であるため、ワーカープール停止前後のレースコンディションに対しても堅牢です。

この設計は、ActionCable のサーバーライフサイクル管理をシンプルに保ちつつ、開発モードで頻繁に起こるリロードシナリオでもリソースリークを防止できる点で有効です。

まとめ

ActionCable::Server::Base#restartconnections_map をクリアするように改修されたことで、開発モードのリロード時に残っていた死んだコネクションが排除され、メモリリークと統計情報の不整合が解消されました。既存のリソース解放と同一のミューテックスブロックで処理を行う設計は、コードベースの一貫性と安全性を高めています。

記事メタデータ

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

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

コードブロックのファイル名付きハイライトは正しい形式ですが、PRリンクが "[PR #57708](URL)" となっており、仕様の "[#57708](URL)" 形式から逸脱しています。リンク自体は機能しますが、形式統一の観点で警告を出します。

対象読者への適合性 ✓ PASS

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

Rails/ActionCable を扱うエンジニア向けの専門的な内容で、初心者向けの余計な説明はなく適切です。

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

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

各セクションが総論・各論・結論の段落構成になっており、トピックセンテンスで始まり、1段落1トピック・6文以内に収まっています。段落間は空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供されたDiffと完全に一致し、追加・削除箇所も正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている用語(connections_map、@mutex、restart など)はPRおよびDiffと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な説明はPRの問題点と修正内容に沿っており、因果関係も正しく記述されています。

事実の突合 ✓ PASS

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

すべての主張はPRの記述・Diffに裏付けられており、外部知識や根拠のない推測はありません。

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

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

PR番号 #57708 が正しく記載されています。数値や固有名詞の誤りはありません。

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

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

記事タイトルはPRの目的「Clear the connections registry on server restart」と内容的に一致しています。

外部知識の正確性 ✓ PASS

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

記事はPR情報のみで構成されており、LTSやリリース日程等の外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現に関する記述はなく、矛盾もありません。