Skip to content

Commit c091113

Browse files
armstrjarerafaelfranca
authored andcommitted
Refactor BroadcastLogger; only execute blocks once
[Fixes rails#49745 rails#52876] [Related rails#51883 rails#49771] This commit refactors the implementation of dispatching method calls to broadcasted loggers. The updated implementation ensures that any block is only executed once. The first logger would recieve the block as per normal, but subsequent loggers would only yield the result of the initial execution. The updated implementation of `dispatch` opened up an opportunity to refactor the way each Logger method is delegatated to broadcasted loggers - simplifying the delegator definitions. Prior to these changes, BroadcastLoggers would iterate each broadcast and re-execute the user provided block for each. The consumer of any Logger would reasonably expect than when calling a method with a block, that the block would only execute a single time. That is, the fact that a Logger is a BroadcastLogger should be irrelevant to consumer. The most common example of this is when using ActiveSupport::TaggedLogging and wrapping behaviour in a `tagged(*tags) { }` block. But this also applies when using the block form `info`, `warn` etc. If a BroadcastLogger is used, and there are multiple loggers being broadcast to, then calling one of these methods with a block would result in the block being executed multiple times. For example: ```ruby broadcasts = ActiveSupport::BroadcastLogger.new( *Array.new(2) { ActiveSupport::Logger.new(STDOUT) } ) number = 0 broadcasts.info { number += 1 "Updated number to #{number}" } # Expected: # Updated number to 1 # Updated number to 1 # # Actual: # Updated number to 1 # Updated number to 2 ``` After these changes, the behaviour of BroadcastLogger reflects the expected behaviour above.
1 parent 1037508 commit c091113

File tree

3 files changed

+75
-75
lines changed

3 files changed

+75
-75
lines changed

activesupport/lib/active_support/broadcast_logger.rb

Lines changed: 46 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ class BroadcastLogger
7676

7777
# Returns all the logger that are part of this broadcast.
7878
attr_reader :broadcasts
79-
attr_reader :formatter
8079
attr_accessor :progname
8180

8281
def initialize(*loggers)
@@ -105,62 +104,36 @@ def stop_broadcasting_to(logger)
105104
@broadcasts.delete(logger)
106105
end
107106

108-
def level
109-
@broadcasts.map(&:level).min
110-
end
111-
112-
def <<(message)
113-
dispatch { |logger| logger.<<(message) }
114-
end
115-
116-
def add(...)
117-
dispatch { |logger| logger.add(...) }
118-
end
119-
alias_method :log, :add
120-
121-
def debug(...)
122-
dispatch { |logger| logger.debug(...) }
123-
end
124-
125-
def info(...)
126-
dispatch { |logger| logger.info(...) }
127-
end
128-
129-
def warn(...)
130-
dispatch { |logger| logger.warn(...) }
131-
end
132-
133-
def error(...)
134-
dispatch { |logger| logger.error(...) }
135-
end
136-
137-
def fatal(...)
138-
dispatch { |logger| logger.fatal(...) }
139-
end
140-
141-
def unknown(...)
142-
dispatch { |logger| logger.unknown(...) }
107+
def local_level=(level)
108+
@broadcasts.each do |logger|
109+
logger.local_level = level if logger.respond_to?(:local_level=)
110+
end
143111
end
144112

145-
def formatter=(formatter)
146-
dispatch { |logger| logger.formatter = formatter }
147-
148-
@formatter = formatter
149-
end
113+
def local_level
114+
loggers = @broadcasts.select { |logger| logger.respond_to?(:local_level) }
150115

151-
def level=(level)
152-
dispatch { |logger| logger.level = level }
116+
loggers.map do |logger|
117+
logger.local_level
118+
end.first
153119
end
154-
alias_method :sev_threshold=, :level=
155120

156-
def local_level=(level)
157-
dispatch do |logger|
158-
logger.local_level = level if logger.respond_to?(:local_level=)
159-
end
121+
LOGGER_METHODS = %w[
122+
<< log add debug info warn error fatal unknown
123+
level= sev_threshold= close
124+
formatter formatter=
125+
] # :nodoc:
126+
LOGGER_METHODS.each do |method|
127+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
128+
def #{method}(...)
129+
dispatch(:#{method}, ...)
130+
end
131+
RUBY
160132
end
161133

162-
def close
163-
dispatch { |logger| logger.close }
134+
# Returns the lowest level of all the loggers in the broadcast.
135+
def level
136+
@broadcasts.map(&:level).min
164137
end
165138

166139
# True if the log level allows entries with severity +Logger::DEBUG+ to be written
@@ -171,7 +144,7 @@ def debug?
171144

172145
# Sets the log level to +Logger::DEBUG+ for the whole broadcast.
173146
def debug!
174-
dispatch { |logger| logger.debug! }
147+
dispatch(:debug!)
175148
end
176149

177150
# True if the log level allows entries with severity +Logger::INFO+ to be written
@@ -182,7 +155,7 @@ def info?
182155

183156
# Sets the log level to +Logger::INFO+ for the whole broadcast.
184157
def info!
185-
dispatch { |logger| logger.info! }
158+
dispatch(:info!)
186159
end
187160

188161
# True if the log level allows entries with severity +Logger::WARN+ to be written
@@ -193,7 +166,7 @@ def warn?
193166

194167
# Sets the log level to +Logger::WARN+ for the whole broadcast.
195168
def warn!
196-
dispatch { |logger| logger.warn! }
169+
dispatch(:warn!)
197170
end
198171

199172
# True if the log level allows entries with severity +Logger::ERROR+ to be written
@@ -204,7 +177,7 @@ def error?
204177

205178
# Sets the log level to +Logger::ERROR+ for the whole broadcast.
206179
def error!
207-
dispatch { |logger| logger.error! }
180+
dispatch(:error!)
208181
end
209182

210183
# True if the log level allows entries with severity +Logger::FATAL+ to be written
@@ -215,21 +188,35 @@ def fatal?
215188

216189
# Sets the log level to +Logger::FATAL+ for the whole broadcast.
217190
def fatal!
218-
dispatch { |logger| logger.fatal! }
191+
dispatch(:fatal!)
219192
end
220193

221194
def initialize_copy(other)
222195
@broadcasts = []
223196
@progname = other.progname.dup
224-
@formatter = other.formatter.dup
225197

226198
broadcast_to(*other.broadcasts.map(&:dup))
227199
end
228200

229201
private
230-
def dispatch(&block)
231-
@broadcasts.each { |logger| block.call(logger) }
232-
true
202+
def dispatch(method, *args, **kwargs, &block)
203+
if block_given?
204+
# Maintain semantics that the first logger yields the block
205+
# as normal, but subsequent loggers won't re-execute the block.
206+
# Instead, the initial result is immediately returned.
207+
called, result = false, nil
208+
block = proc { |*args, **kwargs|
209+
if called then result
210+
else
211+
called = true
212+
result = yield(*args, **kwargs)
213+
end
214+
}
215+
end
216+
217+
@broadcasts.map { |logger|
218+
logger.send(method, *args, **kwargs, &block)
219+
}.first
233220
end
234221

235222
def method_missing(name, ...)

activesupport/test/broadcast_logger_test.rb

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -268,14 +268,28 @@ def info(msg, &block)
268268
assert(logger.foo)
269269
end
270270

271-
test "calling a method that accepts a block" do
272-
logger = BroadcastLogger.new(CustomLogger.new)
271+
test "methods are called on each logger" do
272+
calls = 0
273+
loggers = [CustomLogger.new, FakeLogger.new, CustomLogger.new].each do |logger|
274+
logger.define_singleton_method(:special_method) do
275+
calls += 1
276+
end
277+
end
278+
logger = BroadcastLogger.new(*loggers)
279+
logger.special_method
280+
assert_equal(3, calls)
281+
end
273282

274-
called = false
275-
logger.bar do
276-
called = true
283+
test "calling a method that accepts a block is yielded only once" do
284+
called = 0
285+
logger.info do
286+
called += 1
287+
"Hello"
277288
end
278-
assert(called)
289+
290+
assert_equal 1, called, "block should be called just once"
291+
assert_equal [[::Logger::INFO, "Hello", nil]], log1.adds
292+
assert_equal [[::Logger::INFO, "Hello", nil]], log2.adds
279293
end
280294

281295
test "calling a method that accepts args" do
@@ -356,27 +370,27 @@ def qux(param:)
356370
true
357371
end
358372

359-
def debug(message, &block)
373+
def debug(message = nil, &block)
360374
add(::Logger::DEBUG, message, &block)
361375
end
362376

363-
def info(message, &block)
377+
def info(message = nil, &block)
364378
add(::Logger::INFO, message, &block)
365379
end
366380

367-
def warn(message, &block)
381+
def warn(message = nil, &block)
368382
add(::Logger::WARN, message, &block)
369383
end
370384

371-
def error(message, &block)
385+
def error(message = nil, &block)
372386
add(::Logger::ERROR, message, &block)
373387
end
374388

375-
def fatal(message, &block)
389+
def fatal(message = nil, &block)
376390
add(::Logger::FATAL, message, &block)
377391
end
378392

379-
def unknown(message, &block)
393+
def unknown(message = nil, &block)
380394
add(::Logger::UNKNOWN, message, &block)
381395
end
382396

@@ -385,7 +399,8 @@ def <<(x)
385399
end
386400

387401
def add(message_level, message = nil, progname = nil, &block)
388-
@adds << [message_level, message, progname] if message_level >= local_level
402+
@adds << [message_level, block_given? ? block.call : message, progname] if message_level >= local_level
403+
true
389404
end
390405

391406
def debug?

railties/lib/rails/application/bootstrap.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ module Bootstrap
5959
end
6060
else
6161
Rails.logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase)
62-
broadcast_logger = ActiveSupport::BroadcastLogger.new(Rails.logger)
63-
broadcast_logger.formatter = Rails.logger.formatter
64-
Rails.logger = broadcast_logger
62+
Rails.logger = ActiveSupport::BroadcastLogger.new(Rails.logger)
6563
end
6664
end
6765

0 commit comments

Comments
 (0)