`execute_or_skip`実行後にインストゥルメンターが汚染される問題を修正

rails/rails

FutureResult#execute_or_skipcaller_runsフォールバックポリシーによって呼び出しスレッド上で実行された際に、スレッドのactive_record_instrumenterEventBufferで置き換えられたまま復元されないバグが修正されました。これにより、非同期クエリ実行後もsql.active_record通知が正常に配信されるようになります。

背景

async_query_executor:global_thread_poolに設定して非同期クエリを使用するアプリケーションで、一部リクエストの同期クエリがインストゥルメント・ログ記録されなくなる問題が確認されていました。

原因はグローバルスレッドプールの飽和にあります。global_thread_poolfallback_policy:caller_runsに設定されており、スレッドプールのキャパシティが上限に達した際にはタスクを呼び出しスレッドで実行します。

caller_runsによって呼び出しスレッド(リクエストスレッド)上でタスクが実行されると問題が顕在化します。execute_or_skipActiveSupport::IsolatedExecutionState[:active_record_instrumenter]EventBufferに差し替えますが、元のインストゥルメンターを復元しないため、スレッドのIsolatedExecutionStateに古いEventBufferが永続します。Railsはリクエスト間でこのキーをクリアしないため、そのスレッドが生きている限り以降のsql.active_record通知がすべてEventBufferに飲み込まれ、購読者には配信されなくなります。

技術的な変更

future_result.rbexecute_or_skipメソッドに、インストゥルメンターの保存と復元処理が追加されました。変更はわずか2行で、既存のensureブロックを活用しています。

変更前:

@pool.with_connection do |connection|
  return unless @mutex.try_lock
  begin
    if pending?
      @event_buffer = EventBuffer.new(self, @instrumenter)
      # ...
      execute_query(connection, async: true)
    end
  ensure
    @mutex.unlock
  end
end

変更後:

@pool.with_connection do |connection|
  return unless @mutex.try_lock
  previous_instrumenter = ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]
  begin
    if pending?
      @event_buffer = EventBuffer.new(self, @instrumenter)
      # ...
      execute_query(connection, async: true)
    end
  ensure
    ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = previous_instrumenter
    @mutex.unlock
  end
end

ミューテックスのロック取得後に即座にprevious_instrumenterを保存し、ensureブロックでミューテックスのアンロック前に復元しています。バックグラウンドスレッドで実行される通常ケースではnilを保存してnilに戻すだけであり、実質的にno-opです。

テストではschedule_queryメソッドをモンキーパッチする手法でcaller_runsフォールバックを再現しています。pool.singleton_class上でメソッドを差し替えてexecute_or_skipを呼び出しスレッドで直接実行させ、その後のPost.countsql.active_record通知を正常に発火するかをActiveSupport::Notifications.subscribedで検証しています。

設計判断

EventBufferの有効期間をensureブロックで厳密に囲むアプローチが採用されました。EventBufferはもともとバックグラウンドスレッドでのみ使用される前提で設計されており、スレッドローカルな状態の復元は不要とみなされていました。今回の修正は、その前提がcaller_runsによって崩れる場合があるという事実を受けて、最小限の変更で安全性を確保する方針をとっています。

インストゥルメンターの保存・復元をミューテックスのロック取得直後に行うことで、EventBufferが有効な期間とIsolatedExecutionStateへの書き込み期間を一致させています。これにより、caller_runsでも通常のバックグラウンドスレッド実行でも同一のコードパスが正しく動作する対称的な設計になっています。

まとめ

この修正は、非同期クエリとスレッドプール飽和が重なるという本番環境でのみ再現する複合的な条件を、2行のコード追加で解消しています。IsolatedExecutionStateのような「スレッドローカルに見える」状態を一時的に変更するコードは、caller_runsのようなポリシーによってスレッドの境界が曖昧になる場面でも正しく動作するよう、必ず保存と復元をセットで実装するという教訓が凝縮された変更です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
1577cc64

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→各論(背景、技術的変更、設計判断)→まとめ(結論)という理想的な3部構成が明確に適用されています。必須要素はすべて満たされており、構成は非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライト、PR番号のリンク記法など、カスタムMarkdown構文がすべて正しく使用されています。

対象読者への適合性 ✓ PASS

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

`IsolatedExecutionState`や`instrumenter`などの専門用語を前提としており、専門知識を持つエンジニアという対象読者に完全に適合しています。

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

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

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が非常によく守られています。これにより、高い可読性を実現しています。

Diff内容との照合 ✓ PASS

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

記事内のコードブロックは、提供されたDiff情報を正確に反映しています。テストコードに関する説明もDiffの内容と一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`caller_runs`や`EventBuffer`など、PRで使われている技術用語を正確かつ適切な文脈で使用しています。

説明の技術的正確性 ✓ PASS

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

スレッドプール飽和からインストゥルメンター汚染に至るまでの因果関係の説明が論理的かつ技術的に正確で、PR Descriptionの内容とも完全に一致しています。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiff内のコードによって裏付けられています。「設計判断」セクションはPRの情報を基にした妥当な洞察であり、ハルシネーションは見られません。

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

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

PR番号(#56964)や設定名(`:caller_runs`など)が正確に記載されています。

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

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

記事のタイトルはPRの主題を的確に要約しており、内容との一貫性も保たれています。

外部知識の正確性 ✓ PASS

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

PR情報に含まれない外部知識(バージョンのサポート状況など)の追加はなく、情報源に忠実です。

時間表現の正確性 ✓ PASS

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

事象の前後関係や状態を記述する時間表現は正確で、誤解を招くような表現はありません。