`ActiveSupport::ErrorReporter#report` でコンテキストハッシュをサブスクライバごとに複製
ActiveSupport::ErrorReporter#report が各サブスクライバにコンテキストハッシュを渡す際、deep_dup で複製するよう変更されました。これにより、あるサブスクライバによる context への破壊的変更が他のサブスクライバに影響を与える問題が解消されます。
背景
複数のサブスクライバが登録されている環境で、context ハッシュへの破壊的操作が後続のサブスクライバに副作用をもたらす問題がありました。これまでの実装では、ActiveSupport::ErrorReporter#report を呼び出すと、同一の context ハッシュのインスタンスがすべてのサブスクライバに共有された状態で渡されていました。
エラーレポーティングのサブスクライバでは Foo.bar(baz: context.delete(:baz)) のように、コンテキストから特定のキーを取り出して利用するパターンが一般的です。しかし同一インスタンスを共有していると、先に実行されたサブスクライバによる delete などの破壊的操作の影響を、後続のサブスクライバが受けてしまいます。このような「意図せぬ共有による副作用」はいわゆる footgun(自分の足を撃つような設計上の落とし穴)であり、PR著者はこれを解消すべきと判断しました。
この問題はサブスクライバの実行順序に依存するため、再現が難しく、デバッグコストも高い種類のバグです。
技術的な変更
activesupport/lib/active_support/error_reporter.rb の report メソッド内で、各サブスクライバへの呼び出し時に full_context.deep_dup を渡すよう変更されました。
変更前:
subscriber.report(error, handled: handled, severity: severity, context: full_context, source: source)
変更後:
subscriber.report(error, handled: handled, severity: severity, context: full_context.deep_dup, source: source)
deep_dup を使用しているため、ネストされたハッシュやオブジェクトも含めて完全な独立コピーが各サブスクライバに渡されます。浅いコピー(dup)では、ネストしたハッシュは依然として参照が共有されるため、deep_dup の選択は適切です。また、deep_dup を利用するために require "active_support/core_ext/object/deep_dup" がファイル冒頭に追加されています。
テストでは ContextMutatingSubscriber という専用のサブスクライバクラスが追加されました。このサブスクライバは context.delete(:foo) で context を意図的に破壊的変更し、後続の ErrorSubscriber が受け取る context の独立性を検証しています。テストケースではネストしたハッシュ { foo: { bar: "baz" } } を context に使用することで、deep_dup の深いコピーが機能していることも確認しています。
設計判断
サブスクライバの実装に対して防御的な設計 を採用し、context の不変性をフレームワーク側で保証する方向が選ばれました。
サブスクライバに「context を破壊的変更してはならない」というルールを課すこともできますが、それはサブスクライバの実装者に暗黙の契約を守ることを要求します。deep_dup による複製は呼び出し元のループ1行の変更で済み、サブスクライバ側のコードに一切の制約を課しません。サブスクライバを複数登録するユースケースは十分現実的であり、フレームワークがこの安全性を担保することの意義は大きいです。
パフォーマンス面では deep_dup のコストが生じますが、エラーレポーティングのパスは通常のリクエスト処理に比べて呼び出し頻度が低く、コンテキストハッシュのサイズも限定的であるため、この判断は合理的です。
まとめ
1行の変更と require の追加によって、ActiveSupport::ErrorReporter のサブスクライバ間における context 共有という潜在的なバグを根本から取り除きました。サブスクライバの実装者は context に対して自由に操作ができ、登録順序に依存したデバッグ困難な不具合が発生しにくい、より堅牢なエラーレポーティング基盤となります。