テスト用PostgreSQLアダプタの接続リークを確実なdisconnectで修正

rails/rails

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 セッションの超過は、プール管理の @connectionverify! / reconnect! を経由するケースに起因しており、本PRのスコープ外とされています。

設計判断

ブロックベースのリソース管理パターン を採用し、接続ライフタイムをコードの字義的なスコープと一致させた点が本PRの設計上の核心です。

RubyのGCに接続のクリーンアップを委ねる従来の方式は、単体テストでは動作していても、テストスイート全体を短時間で実行するCI環境では問題を顕在化させます。ensuredisconnect! を保証するヘルパーはファイナライザへの依存を排除し、接続解放のタイミングを決定論的にします。connection_without_insert_returning をブロック受け取り形式に変更したのも同じ方針の一貫であり、既存のテストAPIを内部から整合的に修正するアプローチが取られています。

本PRの変更はテストファイル1つに完全に閉じており、本番コードのパスには触れません。接続リークの残りの発生源(reset_connection 周りなど)は別PRで対処される予定で、各修正を独立してマージできる設計になっています。

まとめ

Ruby的なブロック+ensureイディオムをテストヘルパーに適用するだけで、GC依存の不定なリソース解放を確定的なクリーンアップへ置き換えられることを示した修正です。テストスイートの信頼性は実装コードだけでなくテストコード側の設計にも依存するという事実を、CIの観測可能な障害から出発して体系的に対処した好例といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
a41d4697

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→セクション群(各論)→まとめ(結論)の3部構成が明確に守られています。背景、技術的変更、設計判断の各セクションが論理的に配置されており、非常に理解しやすい構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```ruby:path/to/file.rb)と、PR番号のリンク記法([#57410](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

「GC」「ファイナライザ」「接続リーク」といった専門用語を前提としており、対象読者である専門知識を持つエンジニアに適した技術レベルと表現で記述されています。

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

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

各セクションが総論→各論の構成になっており、かつ各段落がトピックセンテンスで始まっています。1段落1トピックの原則と適切な段落長が守られており、可読性が非常に高いです。

Diff内容との照合 ✓ PASS

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

記事内のコードブロックは、提供されたDiff情報から変更の要点を正確に引用しています。ファイルパスも正しく記載されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「接続リーク」「ファイナライザ」「GC」「ブロックベース」など、PRの内容と文脈に合った正確な技術用語が使用されています。

説明の技術的正確性 ✓ PASS

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

GC依存のクリーンアップが不定なタイミングで実行される問題と、ensureブロックによる確実なリソース解放という解決策の説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

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

CIでのエラー、先行PR(#57410)の存在、計測結果の具体的な数値など、記事内のすべての主張がPRのDescriptionで裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#57433, #57410)、接続数(13〜17から4〜5へ減少)、シード値(37604)、PostgreSQLバージョン(PG 18)など、すべての数値・固有名詞がPR情報と正確に一致しています。

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

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

記事のタイトルはPRの主題「テストローカルなPostgreSQLアダプタの切断による接続リーク防止」を的確に要約しており、内容との整合性も取れています。

外部知識の正確性 ✓ PASS

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

記事内の「PG 18」というバージョン情報はPRのDescriptionに記載されており、PR情報に基づかない外部知識の捏造はありません。

時間表現の正確性 ✓ PASS

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

「断続的に失敗していました」や「先行して試みられましたが」といった時間表現は、PRに記載された事実関係と正確に一致しています。