Skip to content

Commit 5e1a039

Browse files
authored
Merge pull request rails#43282 from theojulienne/guard-against-instrumentation-exceptions
Guard against subscriber exceptions in ActiveSupport::Notifications
2 parents 720f6b1 + 8f76cca commit 5e1a039

File tree

4 files changed

+101
-6
lines changed

4 files changed

+101
-6
lines changed

activerecord/test/cases/adapter_test.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,10 @@ def test_numeric_value_out_of_ranges_are_translated_to_specific_exception
213213
def test_exceptions_from_notifications_are_not_translated
214214
original_error = StandardError.new("This StandardError shouldn't get translated")
215215
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") { raise original_error }
216-
actual_error = assert_raises(StandardError) do
216+
wrapped_error = assert_raises(ActiveSupport::Notifications::InstrumentationSubscriberError) do
217217
@connection.execute("SELECT * FROM posts")
218218
end
219+
actual_error = wrapped_error.exceptions.first
219220

220221
assert_equal original_error, actual_error
221222

activerecord/test/cases/query_cache_test.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,11 +489,13 @@ def test_query_cache_does_not_allow_sql_key_mutation
489489
payload[:sql].downcase!
490490
end
491491

492-
assert_raises FrozenError do
492+
error = assert_raises ActiveSupport::Notifications::InstrumentationSubscriberError do
493493
ActiveRecord::Base.cache do
494494
assert_queries(1) { Task.find(1); Task.find(1) }
495495
end
496496
end
497+
498+
assert error.exceptions.first.is_a?(FrozenError)
497499
ensure
498500
ActiveSupport::Notifications.unsubscribe subscriber
499501
end

activesupport/lib/active_support/notifications/fanout.rb

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77

88
module ActiveSupport
99
module Notifications
10+
class InstrumentationSubscriberError < RuntimeError
11+
attr_reader :exceptions
12+
13+
def initialize(exceptions)
14+
@exceptions = exceptions
15+
exception_class_names = exceptions.map { |e| e.class.name }
16+
super "Exception(s) occurred within instrumentation subscribers: #{exception_class_names.join(', ')}"
17+
end
18+
end
19+
1020
# This is a default queue implementation that ships with Notifications.
1121
# It just pushes events to all registered log subscribers.
1222
#
@@ -59,19 +69,32 @@ def unsubscribe(subscriber_or_name)
5969
end
6070

6171
def start(name, id, payload)
62-
listeners_for(name).each { |s| s.start(name, id, payload) }
72+
iterate_guarding_exceptions(listeners_for(name)) { |s| s.start(name, id, payload) }
6373
end
6474

6575
def finish(name, id, payload, listeners = listeners_for(name))
66-
listeners.each { |s| s.finish(name, id, payload) }
76+
iterate_guarding_exceptions(listeners) { |s| s.finish(name, id, payload) }
6777
end
6878

6979
def publish(name, *args)
70-
listeners_for(name).each { |s| s.publish(name, *args) }
80+
iterate_guarding_exceptions(listeners_for(name)) { |s| s.publish(name, *args) }
7181
end
7282

7383
def publish_event(event)
74-
listeners_for(event.name).each { |s| s.publish_event(event) }
84+
iterate_guarding_exceptions(listeners_for(event.name)) { |s| s.publish_event(event) }
85+
end
86+
87+
def iterate_guarding_exceptions(listeners)
88+
exceptions = nil
89+
90+
listeners.each do |s|
91+
yield s
92+
rescue Exception => e
93+
exceptions ||= []
94+
exceptions << e
95+
end
96+
ensure
97+
raise InstrumentationSubscriberError.new(exceptions) unless exceptions.nil?
7598
end
7699

77100
def listeners_for(name)

activesupport/test/notifications/evented_notification_test.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
module ActiveSupport
66
module Notifications
77
class EventedTest < ActiveSupport::TestCase
8+
# we expect all exception types to be handled, so test with the most basic type
9+
class BadListenerException < Exception; end
10+
811
class Listener
912
attr_reader :events
1013

@@ -27,6 +30,24 @@ def call(name, start, finish, id, payload)
2730
end
2831
end
2932

33+
class BadStartListener < Listener
34+
def start(name, id, payload)
35+
raise BadListenerException
36+
end
37+
38+
def finish(name, id, payload)
39+
end
40+
end
41+
42+
class BadFinishListener < Listener
43+
def start(name, id, payload)
44+
end
45+
46+
def finish(name, id, payload)
47+
raise BadListenerException
48+
end
49+
end
50+
3051
def test_evented_listener
3152
notifier = Fanout.new
3253
listener = Listener.new
@@ -71,6 +92,54 @@ def test_listen_to_everything
7192
], listener.events
7293
end
7394

95+
def test_listen_start_exception_consistency
96+
notifier = Fanout.new
97+
listener = Listener.new
98+
notifier.subscribe nil, BadStartListener.new
99+
notifier.subscribe nil, listener
100+
101+
assert_raises InstrumentationSubscriberError do
102+
notifier.start "hello", 1, {}
103+
end
104+
assert_raises InstrumentationSubscriberError do
105+
notifier.start "world", 1, {}
106+
end
107+
notifier.finish "world", 1, {}
108+
notifier.finish "hello", 1, {}
109+
110+
assert_equal 4, listener.events.length
111+
assert_equal [
112+
[:start, "hello", 1, {}],
113+
[:start, "world", 1, {}],
114+
[:finish, "world", 1, {}],
115+
[:finish, "hello", 1, {}],
116+
], listener.events
117+
end
118+
119+
def test_listen_finish_exception_consistency
120+
notifier = Fanout.new
121+
listener = Listener.new
122+
notifier.subscribe nil, BadFinishListener.new
123+
notifier.subscribe nil, listener
124+
125+
notifier.start "hello", 1, {}
126+
notifier.start "world", 1, {}
127+
assert_raises InstrumentationSubscriberError do
128+
notifier.finish "world", 1, {}
129+
end
130+
assert_raises InstrumentationSubscriberError do
131+
notifier.finish "hello", 1, {}
132+
end
133+
134+
assert_equal 4, listener.events.length
135+
assert_equal [
136+
[:start, "hello", 1, {}],
137+
[:start, "world", 1, {}],
138+
[:finish, "world", 1, {}],
139+
[:finish, "hello", 1, {}],
140+
], listener.events
141+
end
142+
74143
def test_evented_listener_priority
75144
notifier = Fanout.new
76145
listener = ListenerWithTimedSupport.new

0 commit comments

Comments
 (0)