`reset_connection` を軽量化し、プール再生成が必要な呼び出し元を `reset_pool` へ移行
ActiveRecordのテストヘルパー reset_connection が、既存コネクションに対して DISCARD ALL を発行するだけの軽量な実装に変わりました。これにより、PostgreSQLの too many clients already エラーを引き起こしていたコネクションリークが解消されます。
背景
Railsのナイティリ CI で activerecord postgresql ジョブが FATAL: sorry, too many clients already で断続的に失敗していました。調査の結果、PostgresqlRangeTest・PostgresqlEnumTest・PostgresqlCompositeTest・PostgresqlDomainTest の4テストクラスがteardownで呼ぶ reset_connection がリーク源の一つと特定されました。
問題の根本は reset_connection の実装にありました。従来の実装は ActiveRecord::Base.remove_connection + establish_connection でプール全体を入れ替えるものでした。しかし transactional fixtures が有効な場合、フィクスチャ用のトランザクションが既存コネクションを保持しているため、ConnectionPool#disconnect! は排他ロックを取得できずに無言でタイムアウトし、PostgreSQLソケットがリークします。PostgresqlRangeTest だけでも46件のテストがあり、max_connections=100 の環境では積み重なって接続枯渇を起こすのに十分な量でした。
これらの4クラスが reset_connection を呼ぶ目的は、キャッシュされたクエリプランやセッション状態をクリアすることであり、アダプタインスタンスの差し替えは必要ありません。プールの再生成は、後述するように別のテストが必要とする操作です。
技術的な変更
activerecord/test/support/connection_helper.rb にて reset_connection の実装を軽量化し、プール再生成の責務を reset_pool として分離しました。
変更前:
# Used to drop all cache query plans in tests.
def reset_connection
original_connection = ActiveRecord::Base.remove_connection
ActiveRecord::Base.establish_connection(original_connection)
end
変更後:
# Resets state (cached plans, session settings) on the existing connection.
def reset_connection
@connection.reset!
end
# Replaces the connection pool, yielding a fresh adapter instance.
def reset_pool
original_connection = ActiveRecord::Base.remove_connection
ActiveRecord::Base.establish_connection(original_connection)
end
@connection.reset! はPostgreSQLに対して DISCARD ALL を発行します。これにより、コネクションを維持したままキャッシュされたクエリプランとセッション状態がクリアされます。プールの入れ替えが不要なため、transactional fixtures との競合も発生しません。
一方、プールの再生成が必要なケースも存在します。PostgreSQLReferentialIntegrityTest は MissingSuperuserPrivileges モジュールをアダプタに extend するため、teardownでそのモジュールが取り除かれた新しいアダプタインスタンスを得る必要があります。このケースでは reset! では不十分なため、reset_pool が提供する remove_connection + establish_connection の実装が引き続き使われます。実際に以下のファイルが reset_pool に移行しています:
activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb-
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb(6箇所) activerecord/test/cases/adapters/abstract_mysql_adapter/active_schema_test.rbactiverecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
効果の測定は postgres:alpine(PG 18、max_connections=100)でリーク元の4ファイルのみを対象に実施されており、ピーク接続数は修正前の 7〜12 から修正後の 2(フィクスチャのベースライン)まで低減しています。
設計判断
1つのヘルパーに2つの異なる責務が混在していたという問題を、命名によって明示的に分離した設計変更です。
PR #57409 の段階では reset_connection の呼び出し元を個別に @connection.reset! に置き換える方針が検討されました。しかし本PRでは、ヘルパー側の実装を変更することで呼び出し元(リーク源の4ファイル)の変更なしに修正を適用しつつ、プール再生成を必要とする呼び出し元には reset_pool という明示的な名前を与えています。reset_connection という名前が従来「プール全体の再生成」を意味していたことへの技術的負債が解消され、名前と実装のセマンティクスが一致した状態になりました。
また、PRの説明に「4つのリーク元ファイルは diff に現れない」と明記されている点も設計上の意図を示しています。リーク修正のメインパスは reset_connection の再実装であり、それ以外の変更は「プール再生成が本当に必要な呼び出し元を reset_pool に明示的に移行する」作業として分離されています。
まとめ
本PRは、テストヘルパーの実装と命名のミスマッチを修正することで、コネクションリークという実際の障害を解消した変更です。「既存コネクションのリセット」と「プールの再生成」という2つの操作を reset_connection と reset_pool に分離したことで、今後の呼び出し元がそれぞれの副作用を意識した選択ができるようになりました。