Skip to content

Commit a96c723

Browse files
committed
Merge pull request rails#22968 from rabbitt/bugfix/rails41-logger-threadsafety
4.1 Backport of LoggerSilence#silence Threadsafety patch (see: PR rails#20507)
2 parents 766a0cd + e713069 commit a96c723

File tree

3 files changed

+121
-5
lines changed

3 files changed

+121
-5
lines changed

activesupport/lib/active_support/logger.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ def self.broadcast(logger) # :nodoc:
4444
def initialize(*args)
4545
super
4646
@formatter = SimpleFormatter.new
47+
after_initialize if respond_to? :after_initialize
48+
end
49+
50+
def add(severity, message = nil, progname = nil, &block)
51+
return true if @logdev.nil? || (severity || UNKNOWN) < level
52+
super
53+
end
54+
55+
Logger::Severity.constants.each do |severity|
56+
class_eval(<<-EOT, __FILE__, __LINE__ + 1)
57+
def #{severity.downcase}? # def debug?
58+
Logger::#{severity} >= level # DEBUG >= level
59+
end # end
60+
EOT
4761
end
4862

4963
# Simple formatter which only displays the message.
Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,45 @@
11
require 'active_support/concern'
2+
require 'thread_safe'
23

34
module LoggerSilence
45
extend ActiveSupport::Concern
56

67
included do
78
cattr_accessor :silencer
9+
attr_reader :local_levels
810
self.silencer = true
911
end
1012

13+
14+
def after_initialize
15+
@local_levels = ThreadSafe::Cache.new(:initial_capacity => 2)
16+
end
17+
18+
def local_log_id
19+
Thread.current.__id__
20+
end
21+
22+
def level
23+
local_levels[local_log_id] || super
24+
end
25+
1126
# Silences the logger for the duration of the block.
1227
def silence(temporary_level = Logger::ERROR)
1328
if silencer
1429
begin
15-
old_logger_level, self.level = level, temporary_level
30+
old_local_level = local_levels[local_log_id]
31+
local_levels[local_log_id] = temporary_level
32+
1633
yield self
1734
ensure
18-
self.level = old_logger_level
35+
if old_local_level
36+
local_levels[local_log_id] = old_local_level
37+
else
38+
local_levels.delete(local_log_id)
39+
end
1940
end
2041
else
2142
yield self
2243
end
2344
end
24-
end
45+
end

activesupport/test/logger_test.rb

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'stringio'
44
require 'fileutils'
55
require 'tempfile'
6+
require 'active_support/concurrency/latch'
67

78
class LoggerTest < ActiveSupport::TestCase
89
include MultibyteTestHelpers
@@ -113,21 +114,101 @@ def test_should_know_if_its_loglevel_is_below_a_given_level
113114
end
114115

115116
def test_buffer_multibyte
117+
@logger.level = Logger::INFO
116118
@logger.info(UNICODE_STRING)
117119
@logger.info(BYTE_STRING)
118120
assert @output.string.include?(UNICODE_STRING)
119121
byte_string = @output.string.dup
120122
byte_string.force_encoding("ASCII-8BIT")
121123
assert byte_string.include?(BYTE_STRING)
122124
end
123-
125+
124126
def test_silencing_everything_but_errors
125127
@logger.silence do
126128
@logger.debug "NOT THERE"
127129
@logger.error "THIS IS HERE"
128130
end
129-
131+
130132
assert !@output.string.include?("NOT THERE")
131133
assert @output.string.include?("THIS IS HERE")
132134
end
135+
136+
def test_logger_level_per_object_thread_safety
137+
logger1 = Logger.new(StringIO.new)
138+
logger2 = Logger.new(StringIO.new)
139+
140+
level = Logger::DEBUG
141+
assert_equal level, logger1.level, "Expected level #{level_name(level)}, got #{level_name(logger1.level)}"
142+
assert_equal level, logger2.level, "Expected level #{level_name(level)}, got #{level_name(logger2.level)}"
143+
144+
logger1.level = Logger::ERROR
145+
assert_equal level, logger2.level, "Expected level #{level_name(level)}, got #{level_name(logger2.level)}"
146+
end
147+
148+
def test_logger_level_main_thread_safety
149+
@logger.level = Logger::INFO
150+
assert_level(Logger::INFO)
151+
152+
latch = ActiveSupport::Concurrency::Latch.new
153+
latch2 = ActiveSupport::Concurrency::Latch.new
154+
155+
t = Thread.new do
156+
latch.await
157+
assert_level(Logger::INFO)
158+
latch2.release
159+
end
160+
161+
@logger.silence(Logger::ERROR) do
162+
assert_level(Logger::ERROR)
163+
latch.release
164+
latch2.await
165+
end
166+
167+
t.join
168+
end
169+
170+
def test_logger_level_local_thread_safety
171+
@logger.level = Logger::INFO
172+
assert_level(Logger::INFO)
173+
174+
thread_1_latch = ActiveSupport::Concurrency::Latch.new
175+
thread_2_latch = ActiveSupport::Concurrency::Latch.new
176+
177+
threads = (1..2).collect do |thread_number|
178+
Thread.new do
179+
# force thread 2 to wait until thread 1 is already in @logger.silence
180+
thread_2_latch.await if thread_number == 2
181+
182+
@logger.silence(Logger::ERROR) do
183+
assert_level(Logger::ERROR)
184+
@logger.silence(Logger::DEBUG) do
185+
# allow thread 2 to finish but hold thread 1
186+
if thread_number == 1
187+
thread_2_latch.release
188+
thread_1_latch.await
189+
end
190+
assert_level(Logger::DEBUG)
191+
end
192+
end
193+
194+
# allow thread 1 to finish
195+
assert_level(Logger::INFO)
196+
thread_1_latch.release if thread_number == 2
197+
end
198+
end
199+
200+
threads.each(&:join)
201+
assert_level(Logger::INFO)
202+
end
203+
204+
private
205+
def level_name(level)
206+
::Logger::Severity.constants.find do |severity|
207+
Logger.const_get(severity) == level
208+
end.to_s
209+
end
210+
211+
def assert_level(level)
212+
assert_equal level, @logger.level, "Expected level #{level_name(level)}, got #{level_name(@logger.level)}"
213+
end
133214
end

0 commit comments

Comments
 (0)