filter_parametersの組み合わせ爆発を回避
Rails 8.1では、Active Record暗号化を使用する際に Rails.application.config.filter_parameters が指数関数的に増加する問題が修正されました。これにより、属性フィルタリングのパフォーマンスが大幅に改善されます。
背景
#55251 によって filter_attributes に追加された属性が自動的に config.filter_parameters にも追加されるようになりました。しかし、このアプローチには重大な副作用がありました。Active Record暗号化を使用すると、暗号化された属性がモデル名のプレフィックス付きで filter_attributes に追加されます。self.filter_attributes += [attr] というコードにより、既存の config.filter_parameters のすべてのエントリにもモデル名のプレフィックスが付けられてしまうという問題が発生しました。
30個のエントリを持つ config.filter_parameters と、Active Record暗号化を使用する30個のモデルがある場合、組み合わせにより1000個以上のエントリが生成されます。これらのエントリから生成される巨大な正規表現により、属性フィルタリングのパフォーマンスが著しく低下し、実環境では軽量なコントローラーが処理時間の95%を EventReporter のイベントフィルタリングに費やすケースも報告されました。
技術的な変更
activerecord/lib/active_record/core.rb の filter_attributes= メソッドが変更され、親クラスから継承された属性と新規追加された属性を区別するようになりました。
変更前:
def filter_attributes=(filter_attributes)
@inspection_filter = nil
@filter_attributes = filter_attributes
FilterAttributeHandler.sensitive_attribute_was_declared(self, filter_attributes)
end
変更後:
def filter_attributes=(filter_attributes)
@inspection_filter = nil
previous = if @filter_attributes
self.filter_attributes
else
Base.filter_attributes
end
changes = @filter_attributes = filter_attributes
changes -= previous if previous
FilterAttributeHandler.sensitive_attribute_was_declared(self, changes)
end
FilterAttributeHandler.sensitive_attribute_was_declared には、全体のリストではなく新規追加された属性(changes)のみが渡されるようになりました。previous 変数には、現在のクラスに @filter_attributes が設定されている場合はその値が、未設定の場合は Base.filter_attributes が格納されます。差分計算により、既に継承されている属性が重複して処理されることを防ぎます。
filter_attributes ゲッターメソッドも修正され、スーパークラスが Base を継承している場合のみ委譲するようになりました。
def filter_attributes
if @filter_attributes.nil?
if superclass <= Base
superclass.filter_attributes
else
nil
end
else
@filter_attributes
end
end
設計判断
差分のみを通知する方式が採用されました。PR内では全体のリストを渡す方式も検討されましたが、継承チェーン上での重複処理を避けるため、新規追加分のみを FilterAttributeHandler に渡す設計が選ばれています。
テストコードの変更からも設計意図が読み取れます。以前は個々の属性の存在確認を行っていましたが、修正後は config.filter_parameters 全体が期待値と一致するかを検証するようになりました。
assert_equal ["generic_filtered", "credit_card.expires_at", "credit_card.digits"], Rails.application.config.filter_parameters
このテストは、generic_filtered という既存のエントリに対して、CreditCard モデルで追加された expires_at と digits のみがプレフィックス付きで追加されることを確認しています。ApplicationRecord や Base のプレフィックスが付かないことも検証しており、継承チェーンでの重複が排除されていることが分かります。
まとめ
本PRは、Active Record暗号化使用時の config.filter_parameters の指数関数的増加を解消した変更です。継承された属性と新規属性を区別することで、組み合わせ爆発を防ぎ、実環境で報告されていた深刻なパフォーマンス劣化を根本から解決しています。