超長クエリでのRegexp::TimeoutErrorを防ぐ`sql_color`の正規表現最適化

rails/rails

ActiveRecord::LogSubscriber#sql_colorの正規表現を先頭ワード抽出方式に変更し、極端に長いSQLクエリでもRegexp::TimeoutErrorが発生しないようにしました。これにより、大量データの一括INSERT等で発生していた「クエリがログに残らない」問題が解消されます。

背景

sql_colorの旧実装は非アンカー正規表現を含んでおり、極端に長いSQLに対してタイムアウトを引き起こす可能性がありました。#57092で報告されたように、"select " + 'a' * 1000を10万回繰り返した96MBのSQLに対して、正規表現の評価だけで約3.3秒かかるケースが確認されています。

RailsはデフォルトでRegexp.timeout = 1を設定しているため、この遅延はRegexp::TimeoutErrorに発展します。このエラーはインストゥルメンテーション層でキャッチされ、対象クエリはログに一切残らず、代わりに以下のエラーのみが記録されます。

Could not log "sql.active_record" event. Regexp::TimeoutError: regexp match timeout

大容量テキストやバイナリデータを一括挿入するユースケースでは、デバッグが困難になる実害があることがIssueで指摘されています。

技術的な変更

sql_colorの実装を、文字列全体に対する正規表現マッチングから、先頭ワードを1回だけ抽出する方式に刷新しました。

変更前:

def sql_color(sql)
  case sql
  when /\A\s*rollback/mi
    RED
  when /select .*for update/mi, /\A\s*lock/mi
    WHITE
  when /\A\s*select/i
    BLUE
  when /\A\s*insert/i
    GREEN
  when /\A\s*update/i
    YELLOW
  when /\A\s*delete/i
    RED
  when /transaction\s*\Z/i
    CYAN
  else
    MAGENTA
  end
end

変更後:

def sql_color(sql)
  color = if (match = sql.match(/\A\s*(\w+)(?:\s|\Z)/))
    case match[1].downcase
    when "rollback"
      RED
    when "lock"
      WHITE
    when "select"
      if sql.match?(/for update/mi)
        WHITE
      else
        BLUE
      end
    when "insert"
      GREEN
    when "update"
      YELLOW
    when "delete"
      RED
    end
  end

  if color
    color
  else
    if sql.match?(/transaction\s*\Z/i)
      CYAN
    else
      MAGENTA
    end
  end
end

旧実装ではcase/whenの各節がSQLの全文に対してパターンマッチングを試みていました。特に/select .*for update/miはドット.が何にでもマッチするため、長い文字列では線形以上のバックトラックが発生するリスクがあります。また/transaction\s*\Z/i\Zアンカーも文字列末尾への到達が必要で、長い文字列では評価コストが高くなります。

新実装では、まず/\A\s*(\w+)(?:\s|\Z)/で先頭ワードのみを抽出します。このパターンは文字列の先頭に固定されているため、SQLの長さに依存しません。for updateの検出はSELECTに絞った場合のみ実行され、transactionの末尾検索もMAGENTA(デフォルト色)のフォールバックパスにのみ残っています。

テスト側では、SQL_COLORINGSのループを1つのメソッド内に持つ構造から、動的に生成された個別テストメソッドに変更されています。

SQL_COLORINGS.each do |verb, color_regex|
  test "basic_query_logging_coloration_#{verb}" do
    # ...
  end
end

これにより、カラーリングの検証が動詞ごとに独立したテストケースとして実行されるようになり、失敗箇所の特定が容易になります。

設計判断

問題の根本をフォールバックではなく正規表現の計算量削減で解決する方針が採用されました。

#57102では、Regexp::TimeoutErrorrescueして未着色のSQLをフォールバックとしてログする案が提案されていました。このPRはクローズされ、代わりに本PRのアプローチが採用されています。フォールバック方式ではタイムアウトが発生した際に着色なしでログされるため、根本原因が残り続ける点が課題でした。

先頭ワード抽出方式は、/\A/アンカーにより評価範囲を文字列先頭に限定し、SQLの長さに対して実質的に定数時間で動作します。for updatetransactionのパターンは依然として非アンカーですが、前者はSELECT確定後のみ、後者はデフォルト色決定時のみ評価されるため、問題が顕在化する状況は大幅に限定されます。

まとめ

SQLの先頭ワードのみで色を決定するという単純な設計変更により、クエリ長に依存しない安定したログ着色が実現されました。Regexp.timeoutというRailsのグローバル設定がロギング自体を阻害するという微妙なインタラクションを、正規表現の計算量削減によって根本から断ち切った変更といえます。

記事メタデータ

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

この記事は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リンク記法の正確性

ファイル名付きシンタックスハイライト、PR/Issue番号のリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

ActiveRecordの内部実装や正規表現のパフォーマンスに関する専門的な内容で、対象読者であるエンジニアに適した技術レベルです。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内のコード引用は提供されたDiffの内容と正確に一致しており、変更の要点を的確に抜粋しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「非アンカー正規表現」「バックトラック」「インストゥルメンテーション層」など、技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

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

正規表現の計算量に関する説明や、新旧実装の動作原理の解説が技術的に正確で論理的です。

事実の突合 ⚠ WARNING

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

記事内の主張の多くはPR情報で裏付けられていますが、「96MBのSQL」「約3.3秒」「Regexp.timeout = 1」という具体的な数値・設定はPRのDescriptionに直接記載がありません。これらは参照Issueや外部知識からの引用であり、悪質なハルシネーションではありませんが、PR情報外の事実が含まれています。

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

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

PR番号(#57105)、Issue番号(#57092)、クローズされたPR番号(#57102)など、すべての数値・固有名詞は正確です。

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

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

記事タイトルは、PRの「高速化」という内容を、「なぜ」「どのように」という観点から具体的に説明しており、主題と完全に一致しています。

外部知識の正確性 ⚠ WARNING

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

「Regexp.timeout = 1」という記述は、PRに明記されていない外部知識です。文脈理解を助ける有益な情報ですが、ガイドライン上はPR外の情報と見なされます。

時間表現の正確性 ✓ PASS

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

「旧実装」「新実装」などの時間的関係を示す表現は正確で、誤解を招く点はありません。