テスト用PostgreSQLアダプタの接続リークを確実なdisconnectで修正
RailsのCIで発生していた FATAL: sorry, too many clients already エラーの原因の一つを解消するため、PostgreSQLAdapterTest 内で生成されるテストローカルなアダプタに対してブロックベースのヘルパーを導入し、接続を確定的にクリーンアップするようにしました。
背景
Railsのnightlyビルド(activerecord postgresql ジョブ)が FATAL: sorry, too many clients already で断続的に失敗していました。調査の結果、テストコード側に複数のリーク源が存在することが判明し、本PRはそのうちの一つを修正します。
PostgreSQLAdapterTest の多くのテストは、PostgreSQLAdapter.new(...) や connection_without_insert_returning ヘルパー経由で一時的なアダプタを生成し、テスト終了時にローカル変数 connection がスコープから外れるままにしていました。この設計では、PGソケットのクローズがpgライブラリのC層ファイナライザに委ねられ、GCの実行タイミングが不定なためソケットが蓄積します。max_connections=100 の制限に達するまでに複数のテストが同時に未解放ソケットを保持し続けることで、接続数が上限を超えてしまいます。
関連するPRとして #57410 が先行して試みられましたが、診断プローブ側のバインディングリークによって計測値が過大評価されていたため、方法論を修正した上で本PRが改めて提出されました。
技術的な変更
with_postgresql_adapter(config_hash) { |connection| ... } というプライベートヘルパーを新設し、アダプタの生成・yield・ensure ブロックでの disconnect! をセットにすることで、ブロックを抜けた時点で確実に接続を解放するようにしました。
変更前: 各テストは PostgreSQLAdapter.new(...) を直接呼び出し、返された connection をローカル変数に保持したままテストが終了していました。
def test_reconnect_after_bad_connection_on_check_version_with_0_return
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
connection = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new(db_config.configuration_hash.merge(connection_retries: 0))
connection.connect!
connection.pool.instance_variable_set(:@server_version, nil)
connection.raw_connection.stub(:server_version, 0) do
error = assert_raises ActiveRecord::ConnectionNotEstablished do
connection.reconnect!
end
assert_equal "Could not determine PostgreSQL version", error.message
end
assert_nothing_raised do
connection.reconnect!
end
end
変更後: 同じテストが with_postgresql_adapter ブロックに移行され、ブロック終了時に接続が確定的にクローズされます。
def test_reconnect_after_bad_connection_on_check_version_with_0_return
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
with_postgresql_adapter(db_config.configuration_hash.merge(connection_retries: 0)) do |connection|
connection.connect!
connection.pool.instance_variable_set(:@server_version, nil)
connection.raw_connection.stub(:server_version, 0) do
error = assert_raises ActiveRecord::ConnectionNotEstablished do
connection.reconnect!
end
assert_equal "Could not determine PostgreSQL version", error.message
end
assert_nothing_raised do
connection.reconnect!
end
end
end
合わせて connection_without_insert_returning ヘルパーもブロックを受け取る形にリファクタリングされ、内部で新ヘルパーに委譲するようになりました。この変更はテストコードのみに閉じており、本番のアダプタ実装には影響しません。
効果の計測は postgres:alpine(PG 18)、max_connections=100、--seed=37604(ベースライン最悪ケース)、N=5、コンテナ再起動ありの条件で実施されました。ピーク接続数は修正前の 13〜17 から修正後の 4〜5 まで減少しています。残る 2〜3 セッションの超過は、プール管理の @connection が verify! / reconnect! を経由するケースに起因しており、本PRのスコープ外とされています。
設計判断
ブロックベースのリソース管理パターン を採用し、接続ライフタイムをコードの字義的なスコープと一致させた点が本PRの設計上の核心です。
RubyのGCに接続のクリーンアップを委ねる従来の方式は、単体テストでは動作していても、テストスイート全体を短時間で実行するCI環境では問題を顕在化させます。ensure で disconnect! を保証するヘルパーはファイナライザへの依存を排除し、接続解放のタイミングを決定論的にします。connection_without_insert_returning をブロック受け取り形式に変更したのも同じ方針の一貫であり、既存のテストAPIを内部から整合的に修正するアプローチが取られています。
本PRの変更はテストファイル1つに完全に閉じており、本番コードのパスには触れません。接続リークの残りの発生源(reset_connection 周りなど)は別PRで対処される予定で、各修正を独立してマージできる設計になっています。
まとめ
Ruby的なブロック+ensureイディオムをテストヘルパーに適用するだけで、GC依存の不定なリソース解放を確定的なクリーンアップへ置き換えられることを示した修正です。テストスイートの信頼性は実装コードだけでなくテストコード側の設計にも依存するという事実を、CIの観測可能な障害から出発して体系的に対処した好例といえます。