テストの実行順序制御でコネクションリークを修正
LoadAsyncMultiThreadPoolExecutorTest と LoadAsyncMixedThreadPoolExecutorTest において、トランザクショナルフィクスチャとコネクションプールの差し替えタイミングがずれることで発生していたPostgreSQLコネクションリークを、before_setup / after_teardown への移行で修正しました。
背景
Railsのナイトリービルド(activerecord postgresql ジョブ)が FATAL: sorry, too many clients already で断続的に失敗していました。調査の結果、コネクションリークの発生源の一つがこの2つのテストクラスにあることが特定されました。
問題の発端は 5e047f2(「Clean up async test」、2023年2月)です。このコミット以前は setup の ensure 節で establish_connection の呼び出しを即座に元に戻していたため、:multi_thread_pool 設定は実際には機能していませんでした。ensure の除去によりテストが正しく動作するようになりましたが、同時にコネクションリークが顕在化しました。
リークのメカニズムは次の3段階で起きていました:
-
トランザクショナルフィクスチャが
setupの実行前に元の arunit / arunit2 プールに対してトランザクションを開始する -
setup内でBase.establish_connection(config_hash)を呼び出し、コネクションプールを差し替える。この時点でフィクスチャのトランザクションは古いコネクションを掴んだまま残る -
ConnectionPool#disconnect!がそのコネクションを排他的に取得できずにサイレントタイムアウトし、PGソケットが解放されない
このリークにより、テスト実行ごとにピーク時で最大28本のコネクションが消費される状態でした(max_connections=100 の環境での測定値)。他のテストファイルでのリークと合わさることで、接続数の枯渇を引き起こしていました。
技術的な変更
setup / teardown を before_setup / after_teardown に置き換え、super の呼び出し位置を調整することで、フィクスチャのライフサイクルとプール差し替えのタイミングを正しく整合させました。
変更前(LoadAsyncMultiThreadPoolExecutorTest):
def setup
@old_config = ActiveRecord.async_query_executor
ActiveRecord.async_query_executor = :multi_thread_pool
# ...
ActiveRecord::Base.establish_connection(config_hash1)
ARUnit2Model.establish_connection(config_hash2)
end
def teardown
ActiveRecord.async_query_executor = @old_config
clean_up_connection_handler
ActiveRecord::Base.establish_connection(:arunit)
# ...
end
変更後:
def before_setup
@old_config = ActiveRecord.async_query_executor
ActiveRecord.async_query_executor = :multi_thread_pool
# ...
ActiveRecord::Base.establish_connection(config_hash1)
ARUnit2Model.establish_connection(config_hash2)
super # ← superを末尾で呼ぶことでフィクスチャが差し替え後のプールを使う
end
def after_teardown
super # ← superを先頭で呼んでフィクスチャのロールバックを完了させてから
ActiveRecord.async_query_executor = @old_config
clean_up_connection_handler
ActiveRecord::Base.establish_connection(:arunit)
# ...
end
before_setup で super を末尾に置くことで、プールの差し替えが完了した後にトランザクショナルフィクスチャが起動します。これにより、フィクスチャのトランザクションは最初から差し替え後のプールに対して開かれるため、disconnect! が「使用中のコネクション」に遭遇することがなくなります。after_teardown では super を先頭に置き、フィクスチャのロールバックが完了してからプールを元に戻す順序を保証しています。
LoadAsyncMixedThreadPoolExecutorTest でも同様のパターンが適用されており、ENV["RAILS_ENV"] やコネクション設定の復元を含む teardown ロジック全体が after_teardown に移行しています。
設計判断
setup ではなく before_setup を使うという選択は、Minitest のフック実行順序を活用したものです。Minitest では before_setup → setup → (テスト本体) → teardown → after_teardown の順でフックが実行されます。ActiveRecord のトランザクショナルフィクスチャは setup のタイミングでトランザクションを開始するため、それより前に実行される before_setup でプールを差し替えれば、フィクスチャは必ず正しいプールを見ることになります。
コネクションプールのすり合わせや disconnect! のタイムアウト処理を変更するのではなく、テスト側の実行順序を修正するアプローチを選んだことも注目に値します。根本原因は「フィクスチャよりも後にプールを差し替えていた」というタイミングの問題であり、プール実装を変更せずにテストコードの修正だけで解決しています。
まとめ
この修正は、before_setup / after_teardown というMinitest標準のフックを適切に使うことで、コネクションプールとトランザクショナルフィクスチャのライフサイクルを正しく整合させたものです。ピーク時に25〜28本あったコネクション数がフィクスチャのベースライン(2本)まで削減されており、ナイトリービルドの安定化に直接寄与する変更です。