More test fixes: WeakThreadKeyMap concurrency fix and leak‑check test enhancements

rails/rails

Rails のテストスイートで発生していた接続リーク判定のフレークを根本的に解消し、Ruby 3.3.5 未満で使用される WeakThreadKeyMap の競合状態を修正しました。さらに、メンテナンス中の接続保持を正しく扱うテストケースを追加しています。

背景

WeakThreadKeyMap は、Ruby 3.3.5 より前のバージョンで ObjectSpace::WeakKeyMap が GC クラッシュを起こすために採用された代替実装です。しかし、スレッドが死亡した際のエントリ除去が select! に依存していたため、非同期エグゼキュータが存在するマルチスレッド環境でレースコンディションが発生していました。

先行 PR #57744#57750 では、メンテナンススレッドのチェックアウト順序を調整してフレークの抑止を試みましたが、別のテストで新たな不安定要因が確認されました。これにより、データ構造自体のスレッド安全性を高める根本的な対策が必要となりました。

この背景を受けて、WeakThreadKeyMap の内部実装をスレッド安全なマップに置き換え、同時実行時の競合を防止することが本 PR の主目的です。

技術的な変更

WeakThreadKeyMap の内部ストレージを Concurrent::Map に変更し、スレッド間で安全に参照・更新できるようにしました。これにより、select! による一括クリーニングから、個別にスレッドの存命確認を行うロジックへと移行します。

@@ -136,7 +136,7 @@ class WeakThreadKeyMap # :nodoc:
           # FIXME: On 3.3 we could use ObjectSpace::WeakKeyMap
           # but it currently causes GC crashes: https://github.com/byroot/rails/pull/3
           def initialize
-            @map = {}
+            @map = Concurrent::Map.new
           end

[]= メソッドでは、従来の @map.select! { |c, _| c&.alive? } を廃止し、each_pair で走査しながら死亡スレッドのエントリを削除するコードに置き換えました。変更後は明示的に delete を呼び出すため、同時実行時の不整合が起きません。

@@ -148,7 +148,9 @@ def []=(key, value)
-            @map.select! { |c, _| c&.alive? }
+            @map.each_pair do |thread, _|
+              @map.delete(thread) unless thread&.alive?
+            end
             @map[key] = value
           end

テスト側では check_connection_leaks にオプション引数 connection_pools = nil を導入し、必要に応じて対象プールだけを走査できるようにしました。既存の呼び出しは引数が省略されるため従来通り全プールをチェックします。

@@ -39,13 +39,13 @@ def after_teardown
       check_connection_leaks
     end
-
-    def check_connection_leaks
+    def check_connection_leaks(connection_pools = nil)
       return if in_memory_db?
@@
-      ActiveRecord::Base.connection_handler.each_connection_pool do |pool|
+      (connection_pools || ActiveRecord::Base.connection_handler.each_connection_pool).each do |pool|
         # Ensure all in flights tasks are completed.
         # Otherwise they may still hold a connection.
         if pool.async_executor

さらに、activerecord/test/cases/test_case_test.rb に2つのテストケースが追加され、メンテナンス中や非同期エグゼキュータが稼働中でも接続リークと誤判定しないことを検証しています。テストは Concurrent::Event でスレッド同期を制御し、レースコンディションを再現しつつ期待通りに動作することを確認します。

@@
-      release_maintenance_latch = Concurrent::CountDownLatch.new(1)
-      leak_check_attempted_latch = Concurrent::CountDownLatch.new(1)
+      release_maintenance = Concurrent::Event.new
+      leak_check_attempted = Concurrent::Event.new
@@
-      maintenance_thread = Thread.new do
-        Thread.current.name = "AR Pool Reaper"
-
-        pool.reaper_lock do
-          pool.send(:checkout_for_maintenance, conn)
-          release_maintenance_latch.wait
-          pool.send(:return_from_maintenance, conn)
-        end
-      end
+      maintenance_thread = Thread.new do
+        Thread.current.name = "AR Pool Reaper"
+
+        pool.reaper_lock do
+          pool.send(:checkout_for_maintenance, conn)
+          release_maintenance.wait
+          pool.send(:return_from_maintenance, conn)
+        end
+      end

これらの変更は、内部データ構造の安全性向上とテストの信頼性強化を同時に実現し、既存のアプリケーションコードへの影響はありません。

設計判断

Concurrent::Map への置き換えは、外部インターフェイス([][]=)を保ちつつスレッド安全性を確保する最小侵入的な選択でした。既存コードは API が変わらないため、利用側の改修は不要です。

check_connection_leaks に引数を追加したのは、テストから特定プール集合を注入して非同期エグゼキュータの影響を切り離すための設計です。デフォルトは nil で従来通り全プールを走査し、公開 API の互換性を維持しています。

新規テストでは Concurrent::EventConcurrent::CountDownLatch といった同期プリミティブを用いて、レース条件を deterministic に再現できるようにしました。これにより、将来的に他の非同期機構が追加された際にも同様の検証が容易になります。

まとめ

WeakThreadKeyMap の内部実装を Concurrent::Map に置き換えることで、Ruby 3.3.5 未満での同時実行バグを根本的に解消しました。加えてテスト側の拡張と新規テストケースにより、接続リーク判定の誤検出が防止され、テストスイートの安定性が向上します。これらの変更は公開 API を破壊せず、既存アプリケーションへの影響を最小限に抑えた形で導入されています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
63b19565

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的変更、設計判断(任意)、まとめの5つの要素がすべて揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは ````ruby:ファイルパス```` 形式で正しく記載され、GitHubリンクは #番号 と URL のリンク形式で正しく記述されています。

対象読者への適合性 ✓ PASS

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

内容は Rails のコア開発者や高度な Ruby エンジニア向けで、初心者向けの過度な解説はなく適切です。

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

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

各セクションは総論・各論・結論の段落構成が守られ、段落の冒頭がトピックセンテンスとなり、1段落1トピックで分かりやすく書かれています。段落長も適切です。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供された Diff と完全に一致し、ファイル名・変更内容ともに正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

WeakThreadKeyMap、Concurrent::Map、Event などの用語は PR と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な説明は Diff の変更点と整合しており、因果関係も論理的に正しいです。

事実の突合 ✓ PASS

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

記事の全ての主張は PR のタイトル、説明、Diff に裏付けられており、捏造や推測は見られません。

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

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

PR 番号 #57744、#57750、#57768 が正しく記載されており、数値の誤りはありません。

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

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

記事タイトルは PR タイトル「More test fixes」をベースにしつつ、内容を具体化していて一致しています。

外部知識の正確性 ✓ PASS

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

バージョンサポートやリリーススケジュール等の外部知識は一切含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記述と整合しています。