ActionCableの再起動時に接続レジストリをクリアしてメモリリークを防止
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 が呼び出されるため、死んだコネクションが蓄積し #connections や open_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_connection が restart 前に走っていた場合でも競合は発生せず、安全にレジストリをリセットできます。このシンプルなクリア処理により、追加の同期ロジックや複雑な状態管理は不要となります。
設計判断
本変更は、@mutex で保護された既存のリソース解放フローに connections_map.clear を組み込むという、一貫した設計パターンを踏襲しています。直前にマージされた PR #57700 で @heartbeat_timer の停止が同様に追加されたことと同様です。
clear を選択した背景は、レジストリの状態をリセットするだけで十分であり、個々のエントリを削除するよりもコードが簡潔でありながら安全性が確保できる点です。clear は idempotent であるため、ワーカープール停止前後のレースコンディションに対しても堅牢です。
この設計は、ActionCable のサーバーライフサイクル管理をシンプルに保ちつつ、開発モードで頻繁に起こるリロードシナリオでもリソースリークを防止できる点で有効です。
まとめ
ActionCable::Server::Base#restart が connections_map をクリアするように改修されたことで、開発モードのリロード時に残っていた死んだコネクションが排除され、メモリリークと統計情報の不整合が解消されました。既存のリソース解放と同一のミューテックスブロックで処理を行う設計は、コードベースの一貫性と安全性を高めています。