トランザクション開始をインストルメンテーション前に実体化

rails/rails

遅延トランザクション実体化(lazy transaction materialization)において、BEGIN/SAVEPOINTクエリの実行時間が二重計上されていた問題が修正されました。トランザクション内の最初のクエリに対する sql.active_record 通知が、BEGINとクエリ本体の両方の実行時間を含んでいたため、実際には1往復のところが2往復に見える状態でした。

背景

遅延トランザクション実体化では、トランザクションブロック内で最初のクエリが実行されるまでBEGINの発行を遅延させます。この機能は 542f095 で導入され、接続検証をチェックアウト時から最初のクエリ実行時まで遅延させることで、接続関連の例外に対する再試行インフラを提供しています。

しかし、この実装ではBEGINの実体化が log() メソッドのインストルメンテーションラッパー内で行われていたため、最初のクエリの sql.active_record 通知にBEGINの実行時間が含まれてしまっていました。BEGINは独自の通知も発行するため、その実行コストが二重に報告される結果となっていました。

別のデータセンターのデータベースに対してクエリを実行した際、トランザクション内の最初のクエリが常に2往復を要しているように見えることから、この問題が発見されました。

技術的な変更

activerecord/lib/active_record/connection_adapters/abstract/database_statements.rbexecute_intent メソッドが修正され、トランザクションの実体化がインストルメンテーション前に行われるようになりました。

変更前:

def execute_intent(intent)
  log(intent) do |notification_payload|
    intent.notification_payload = notification_payload
    with_raw_connection(allow_retry: intent.allow_retry, materialize_transactions: intent.materialize_transactions) do |conn|
      result = perform_query(conn, intent)
      intent.raw_result = result
      handle_warnings(result, intent.processed_sql)
    end
  end
end

変更後:

def execute_intent(intent)
  should_dirty = false

  if intent.materialize_transactions
    # These can raise locally (e.g., ReadOnlyError). Validate before BEGIN.
    intent.processed_sql
    intent.type_casted_binds
    materialize_transactions
  end

  log(intent) do |notification_payload|
    intent.notification_payload = notification_payload
    with_raw_connection(allow_retry: intent.allow_retry, materialize_transactions: false) do |conn|
      should_dirty = intent.materialize_transactions
      result = perform_query(conn, intent)
      intent.raw_result = result
      handle_warnings(result, intent.processed_sql)
    end
  end
ensure
  dirty_current_transaction if should_dirty
end

log() の呼び出し前に materialize_transactions を実行することで、BEGINが独自の通知とともに完了してから、クエリのインストルメンテーションが開始されます。with_raw_connectionmaterialize_transactions パラメータは false に設定され、実体化は既に完了しているため重複実行を防ぎます。

intent.processed_sqlintent.type_casted_binds の事前呼び出しは、ReadOnlyError のようなローカルで発生する例外を、BEGINの発行前に検証するためです。これにより、トランザクションが不必要に開始されることを防ぎます。

テストによる検証

activerecord/test/cases/transaction_instrumentation_test.rb に追加されたテストは、イベントの重複が解消されたことを確認しています。

def test_sql_events_do_not_overlap
  events = []
  subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |event|
    events << event
  end

  Topic.transaction { Topic.first }

  assert_equal 3, events.size
  begin_event, select_event, commit_event = events

  assert begin_event.payload[:sql].start_with?("BEGIN")
  assert select_event.payload[:sql].start_with?("SELECT")
  assert commit_event.payload[:sql].start_with?("COMMIT")

  assert_operator begin_event.time, :<=, begin_event.end
  assert_operator begin_event.end, :<=, select_event.time
  assert_operator select_event.time, :<=, select_event.end
  assert_operator select_event.end, :<=, commit_event.time
  assert_operator commit_event.time, :<=, commit_event.end
end

このテストは、BEGIN、SELECT、COMMITの各イベントが時系列順に並び、互いに重複していないことを検証します。begin_event.end <= select_event.time のアサーションにより、BEGINの完了後にSELECTのインストルメンテーションが開始されることが保証されます。

SAVEPOINTを使用するネストしたトランザクションについても、同様のテストが追加されています。

エラーハンドリングの改善

activerecord/test/cases/transactions_test.rb に追加されたテストは、実体化前のエラーがトランザクション状態を汚染しないことを確認しています。

def test_locally_rejected_query_does_not_materialize_or_dirty_transaction
  ActiveRecord::Base.transaction do
    connection = ActiveRecord::Base.lease_connection
    ActiveRecord::Base.while_preventing_writes do
      assert_raises(ActiveRecord::ReadOnlyError) { Topic.create!(title: "test") }
    end
    assert_not connection.current_transaction.materialized?
    assert_not connection.current_transaction.dirty?
  end
end

ReadOnlyError が発生した場合、トランザクションは実体化されず、dirtyフラグも設定されません。これは intent.processed_sql の呼び出しによる事前検証が機能していることを示しています。

接続が失われた場合の動作も検証されています:

def test_failed_materialization_does_not_dirty_transaction
  ActiveRecord::Base.transaction do
    connection = ActiveRecord::Base.lease_connection
    connection.stub(:begin_db_transaction, -> (*) { raise ActiveRecord::ConnectionFailed, "connection lost" }) do
      raised = assert_raises(ActiveRecord::ConnectionFailed) { Topic.first }
      assert_equal "connection lost", raised.message
    end
    assert_not connection.current_transaction.dirty?
  end
end

BEGINの実行中に ConnectionFailed が発生した場合、トランザクションはdirtyとしてマークされません。ensure 節での dirty_current_transaction 呼び出しは、should_dirty フラグが実体化成功後にのみ設定されるため、適切に制御されています。

設計判断

実体化をインストルメンテーション前に移動する設計により、以下の3つの目標が同時に達成されています。

  1. 正確なタイミング測定: 各SQLイベントが独立した時間範囲を持ち、実行コストの重複計上が解消される
  2. 早期エラー検出: ローカルで検出可能なエラー(ReadOnlyErrorなど)をトランザクション開始前に検証する
  3. 状態の一貫性: 実体化失敗時にトランザクションをdirtyとしてマークしない

should_dirty フラグを使用した制御フローは、実体化の成功と失敗を明確に区別します。ensure 節での条件付き dirty_current_transaction 呼び出しは、実体化が正常に完了した場合のみトランザクションをdirtyとしてマークする設計です。

まとめ

本PRは、遅延トランザクション実体化における計測の正確性を改善しました。トランザクションの実体化をインストルメンテーション前に移動することで、BEGIN/SAVEPOINTとクエリ本体の実行時間が明確に分離され、パフォーマンス分析の精度が向上します。また、エラー検証の前倒しにより、不必要なトランザクション開始を防ぐ副次的な効果も得られています。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事は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```)とGitHubのコミットID・PR番号のリンク記法が、ガイドラインに準拠して正しく使用されています。

対象読者への適合性 ✓ PASS

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

内容はRailsの内部実装に関する高度なもので、専門知識を持つエンジニアという対象読者に完全に適合しています。冗長な説明はなく、簡潔で的確です。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内で引用されているすべてのコードブロックは、提供されたDiff情報と完全に一致しています。コードの改変や省略はなく、正確に引用されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「遅延トランザクション実体化」「インストルメンテーション」「dirtyフラグ」などの技術用語が、PRの文脈に沿って正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

インストルメンテーション前に実体化処理を移動したことによる影響や、エラーハンドリングの改善に関する説明は、Diffのコード変更と論理的に整合しており、技術的に正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題の背景、原因、解決策)は、PRのDescriptionやDiff内のコード・コメントによって裏付けられています。ハルシネーション(捏造)は見られません。

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

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

PR番号(#56791)、コミットID(542f095)、ファイルパスなどの数値・固有名詞はすべて正確に記載されています。

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

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

記事のタイトル「トランザクション開始をインストルメンテーション前に実体化」は、PRのタイトル「Materialize transactions before instrumentation」を忠実に反映しており、内容と完全に一致しています。

外部知識の正確性 ✓ PASS

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

記事の内容は提供されたPR情報に完全に準拠しており、バージョンサポート状況やリリース予定など、PRに記載のない外部知識の持ち込みはありません。

時間表現の正確性 ✓ PASS

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

PR内で言及されている過去の事象(機能導入の経緯など)について、時間表現を歪曲することなく正確に記述されています。