StructuredEventSubscriber の debug_only がサブクラス間で漏れないように修正

rails/rails

ActiveSupport::StructuredEventSubscriber の debug_only がサブクラス間で共有されていた配列に依存し、意図しないメソッドが全体に漏れるバグを、配列の破壊的操作から非破壊的再代入へ変更することで解消しました。

背景

ActiveSupport::StructuredEventSubscriberdebug_methodsclass_attribute で定義し、デフォルトは空配列 [] です。debug_only が呼び出されるたびに self.debug_methods << method が実行されるため、同一オブジェクトの配列がすべてのサブクラスで共有され、メソッド名が汚染されてしまいます。

再現例として以下のコードを実行すると、二つのサブクラスが互いのメソッドを取得し、基底クラスまで汚染された配列が残ります。

sx = Class.new(ActiveSupport::StructuredEventSubscriber) { def foo(e); end; debug_only :foo }
sy = Class.new(ActiveSupport::StructuredEventSubscriber) { def bar(e); end; debug_only :bar }

sx.debug_methods      # => [:foo, :bar]   (期待は [:foo])
sy.debug_methods      # => [:foo, :bar]   (期待は [:bar])
ActiveSupport::StructuredEventSubscriber.debug_methods # => [:foo, :bar] (基底も汚染)

この状態では、フレームワークが内部で debug_only に登録したメソッド(例: :sql, :render_template)が、ユーザー実装のサブクラスにまで影響し、本番環境で本来通知すべきイベントが抑止される危険があります。

技術的な変更

debug_only の実装を 破壊的変更 から 非破壊的再代入 に置き換えるだけで、サブクラスごとに独立した配列が生成されるようになりました。

@@ -48,7 +48,7 @@ def set_silenced_events
         end

         def debug_only(method)
-          self.debug_methods << method
+          self.debug_methods += [method]
           set_silenced_events
         end
     end

self.debug_methods += [method]class_attribute の書き込みメソッドを通して新しい配列を代入するため、デフォルト配列がコピーされ、各サブクラスが独自の debug_methods を保持します。これにより、元の共有オブジェクトは変更されず、汚染は起きません。

テストも同様に修正され、サブクラス間でのリークを検証する新しいテスト test_debug_only_does_not_leak_across_subclasses が追加されました。また、既存テスト test_debug_only_methods ではサブスクライバのアタッチ先を実際のサブクラス TestSubscriber に変更し、リークが無い前提で正しく動作することを確認しています。

@@
   teardown do
     ActiveSupport.event_reporter.debug_mode = @old_debug_mode
+    TestSubscriber.detach_from :test
+    ActiveSupport::StructuredEventSubscriber.detach_from :test
   end
@@
-    ActiveSupport::StructuredEventSubscriber.attach_to :test, @subscriber
+    TestSubscriber.attach_to :test, @subscriber
@@
+  def test_debug_only_does_not_leak_across_subclasses
+    base_methods = ActiveSupport::StructuredEventSubscriber.debug_methods.dup
+
+    subscriber_a = Class.new(ActiveSupport::StructuredEventSubscriber) do
+      def foo(event); end
+      debug_only :foo
+    end
+
+    subscriber_b = Class.new(ActiveSupport::StructuredEventSubscriber) do
+      def bar(event); end
+      debug_only :bar
+    end
+
+    assert_equal [:foo], subscriber_a.debug_methods
+    assert_equal [:bar], subscriber_b.debug_methods
+    assert_equal base_methods, ActiveSupport::StructuredEventSubscriber.debug_methods
+  end

この変更はコードサイズが僅か1行の差分で済み、既存の API 互換性を保持しつつバグを根本的に排除します。

設計判断

非破壊的再代入 を選択したのは、class_attribute が内部でコピーを返す特性を活かし、個別の配列インスタンスを自動的に生成させるためです。LogSubscriber.subscribe_log_level でも同様に self.log_levels = ... と再代入している点と同調し、コードベース全体で一貫した設計パターンを維持しています。

このアプローチは 後方互換性 を損なわず、debug_only を呼び出す既存のサブクラスはそのまま動作します。配列をその場で << する破壊的操作は削除され、将来的に同様の問題が別属性で再発するリスクも低減されます。

結果として、ActiveSupport のサブスクライバ機構はサブクラスごとに安全にデバッグ対象を限定でき、フレームワーク本体とユーザー実装の境界が明確になります。

まとめ

debug_only が内部配列を破壊的に変更していたために起きていたサブクラス間のメソッド漏れを、self.debug_methods += [method] という非破壊的再代入に置き換えるだけで解決しました。テストも拡充され、変更の安全性が検証されています。このシンプルな修正は、class_attribute の特性を正しく活用した設計判断の好例と言えるでしょう。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
61fcff75

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文と背景・技術的変更・設計判断・まとめの構成が整っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

GitHubリンクの記法が `[PR #57676](URL)` となっており、推奨フォーマット `[ #57676 ](URL)` ではありません。コードブロック自体は正しい形式です。

対象読者への適合性 ✓ PASS

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

対象はRails内部実装に詳しいエンジニア向けで、不要な初心者向け説明はありません。

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

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

各セクションの冒頭が要点を示すトピックセンテンスとなっており、段落は1トピック・6文以内で空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事内のdiffコードは提供されたDiffと完全に一致し、変更箇所・テスト追加内容とも合致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`class_attribute`、`debug_methods`、`debug_only` などの用語はPRと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な説明はPRの記述と整合しており、因果関係も正しく示されています。

事実の突合 ✓ PASS

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

全ての主張はPRタイトル、説明、Diffで裏付けられており、捏造や推測はありません。

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

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

PR番号 #57676 が正しく記載されています。他の数値・固有名詞の誤りはありません。

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

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

記事タイトルはPRの内容を正確に日本語化しており、一致しています。

外部知識の正確性 ✓ PASS

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

PRに記載されていないバージョンやリリース情報等の外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の記載はなく、PR情報と矛盾する表現もありません。