Skip to content

Commit 8afcbb6

Browse files
authored
Guard against subscriber exceptions in ActiveSupport::Notifications
1 parent e21c38e commit 8afcbb6

File tree

2 files changed

+95
-4
lines changed

2 files changed

+95
-4
lines changed

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 => 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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
module ActiveSupport
66
module Notifications
77
class EventedTest < ActiveSupport::TestCase
8+
class BadListenerException < RuntimeError; end
9+
810
class Listener
911
attr_reader :events
1012

@@ -27,6 +29,24 @@ def call(name, start, finish, id, payload)
2729
end
2830
end
2931

32+
class BadStartListener < Listener
33+
def start(name, id, payload)
34+
raise BadListenerException
35+
end
36+
37+
def finish(name, id, payload)
38+
end
39+
end
40+
41+
class BadFinishListener < Listener
42+
def start(name, id, payload)
43+
end
44+
45+
def finish(name, id, payload)
46+
raise BadListenerException
47+
end
48+
end
49+
3050
def test_evented_listener
3151
notifier = Fanout.new
3252
listener = Listener.new
@@ -71,6 +91,54 @@ def test_listen_to_everything
7191
], listener.events
7292
end
7393

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

0 commit comments

Comments
 (0)