パラメータフィルタの冗長なパターンを最適化
Active Recordの暗号化機能が自動登録するフィルタパターンは、暗号化カラムの増加に伴い肥大化し、パフォーマンスに深刻な影響を与えていました。本PRは、プリコンパイル段階で冗長なパターンを削除し、この問題を解決しています。
背景
Active Recordの暗号化機能は、暗号化された全ての属性に対して自動的にdeepパターン(user.password のようなネストしたパス)を登録します。例えば _token サフィックスを持つ暗号化カラムが多数存在する場合、user.api_token、user.access_token、admin.session_token といったパターンが個別に登録されますが、既に token という単純なパターンが登録されていればこれらは全て冗長です。
フィルタパターンのリストが急速に肥大化すると、各リクエストでのパラメータフィルタリング処理のコストが増大します。#56762 は、プリコンパイルステップで冗長なパターンを削除することでこの問題に対処しています。
技術的な変更
ActiveSupport::ParameterFilter.precompile_filters メソッドが、パターンの包含関係を検証する処理を追加しました。
変更前:
patterns.map! do |pattern|
pattern.is_a?(Regexp) ? pattern : "(?i:#{Regexp.escape pattern.to_s})"
end
deep_patterns = patterns.extract! { |pattern| pattern.to_s.include?("\\.") }
filters << Regexp.new(patterns.join("|")) if patterns.any?
filters << Regexp.new(deep_patterns.join("|")) if deep_patterns.any?
変更後:
regexps, patterns = patterns.partition { |filter| filter.is_a?(Regexp) }
patterns.map! do |p|
p.is_a?(Symbol) ? p.name : p.to_s
end
patterns.sort_by! { |p| [p.count("."), p.size] }
patterns.each do |pattern|
unless regexps.any? { |r| r.match?(pattern) }
regexps << Regexp.new(Regexp.escape(pattern), Regexp::IGNORECASE)
end
end
filters << Regexp.union(regexps)
主要な変更点は以下の3つです:
-
パターンの分離: まず既存のRegexpパターンを
regexps配列に分離し、文字列/シンボルパターンのみを処理対象とします -
ソート処理: パターンをドット数と長さでソートし、より広範なパターン(
passwordなど)が先に処理されるようにします -
冗長性チェック: 各パターンを追加する前に、既存の正規表現パターンにマッチするかを
regexps.any? { |r| r.match?(pattern) }で検証します。マッチする場合は冗長とみなして追加をスキップします
テストケースでは、この最適化の効果が検証されています:
test "precompile_filters eliminate dead patterns" do
assert_equal [/token/i], ActiveSupport::ParameterFilter.precompile_filters(["user.token", "token"])
assert_equal [/password/i], ActiveSupport::ParameterFilter.precompile_filters(["user_password", "password"])
end
["user.token", "token"] は [/token/i] に、["user_password", "password"] は [/password/i] に集約されます。より広範なパターンが存在する場合、特定的なパターンは自動的に削除される仕組みです。
設計判断
deepパターンと通常パターンの区別を廃止し、全てを統合した単一の正規表現にまとめる方式 が採用されました。
変更前は patterns と deep_patterns を別々の正規表現として保持していましたが、変更後は Regexp.union で単一の正規表現に統合しています。テストの変更からもこの統合が確認できます:
# 変更前: 2つの正規表現が生成される
assert_equal 2, precompiled.grep(Regexp).length
# 変更後: 1つの正規表現に統合される
assert_equal 1, precompiled.grep(Regexp).length
この設計により、パターンマッチングの処理が一度の正規表現評価で完結します。また、ソート処理(sort_by! { |p| [p.count("."), p.size] })により、ドット数が少ない(=より広範な)パターンを優先的に処理し、後続の特定的なパターンを効率的に排除する仕組みを実現しています。
まとめ
本PRは、Active Recordの暗号化機能による大量のフィルタパターン登録という実用上の課題に対し、プリコンパイル段階での冗長性削除という解決策を提示しています。パターンの包含関係を検証するシンプルなロジックの追加により、パフォーマンス劣化を引き起こす冗長なパターンを自動的に排除し、フィルタリング処理の効率化を実現しました。