From 457612839344d761f143e1ab2d8eea551b76efe2 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 1 Oct 2025 11:35:06 -0400 Subject: [PATCH 1/2] Use ObjectSpace::WeakKeyMap for level_override This allows fiber keys to be GCed and removed from the map. Otherwise, fibers that call `#with_level` create a memory leak if they are killed without running their ensure blocks. --- lib/logger.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/logger.rb b/lib/logger.rb index be00457..d9d1dcd 100644 --- a/lib/logger.rb +++ b/lib/logger.rb @@ -379,6 +379,14 @@ class Logger include Severity + # Must respond to .new and return a Hash-like object. + # The returned object must respond to #[], #[]=, #delete. + # + # ObjectSpace::WeakKeyMap when supported. + OverrideMap = + defined?(ObjectSpace::WeakKeyMap) ? ObjectSpace::WeakKeyMap : Hash + private_constant :OverrideMap + # Logging severity threshold (e.g. Logger::INFO). def level level_override[level_key] || @level @@ -606,7 +614,7 @@ def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG, self.datetime_format = datetime_format self.formatter = formatter @logdev = nil - @level_override = {} + @level_override = OverrideMap.new return unless logdev case logdev when File::NULL @@ -789,7 +797,7 @@ def level_override does not call super probably ;;; end - @level_override ||= {} + @level_override ||= OverrideMap.new end def level_key From 1b4e47260307d978714878074420939a2d3922ce Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 1 Oct 2025 12:42:13 -0400 Subject: [PATCH 2/2] Clear level overrides in #dup and #clone Cloning loggers can be used, for example, to create different loggers for different classes/components/subsystems that share the same log_dev and other configuration (possibly a subclass of Logger), but with different verbosity levels, formatters, etc. But, that doesn't work if they share `@level_override`. Rather than use OverrideMap.new, the existing `@level_override` is cloned and cleared, to preserve `@level_override`'s class. --- lib/logger.rb | 7 ++++++- test/logger/test_severity.rb | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/logger.rb b/lib/logger.rb index d9d1dcd..7a9a155 100644 --- a/lib/logger.rb +++ b/lib/logger.rb @@ -380,7 +380,7 @@ class Logger include Severity # Must respond to .new and return a Hash-like object. - # The returned object must respond to #[], #[]=, #delete. + # The returned object must respond to #[], #[]=, #delete, #dup, and #clear. # # ObjectSpace::WeakKeyMap when supported. OverrideMap = @@ -800,6 +800,11 @@ def level_override @level_override ||= OverrideMap.new end + def initialize_copy(other) + super + @level_override = @level_override&.clone&.clear + end + def level_key Fiber.current end diff --git a/test/logger/test_severity.rb b/test/logger/test_severity.rb index fb26939..3a4251e 100644 --- a/test/logger/test_severity.rb +++ b/test/logger/test_severity.rb @@ -31,20 +31,31 @@ def test_fiber_local_level logger.level = INFO # default level other = Logger.new(nil) other.level = ERROR # default level + clone = logger.clone # should not be chaged assert_equal(other.level, ERROR) logger.with_level(:WARN) do assert_equal(other.level, ERROR) assert_equal(logger.level, WARN) + assert_equal(clone.level, INFO) + assert_equal(logger.dup.level, INFO) + assert_equal(logger.clone.level, INFO) + logger.clone.with_level(:FATAL) do + assert_equal(logger.level, WARN) + end logger.with_level(DEBUG) do # verify reentrancy assert_equal(logger.level, DEBUG) + assert_equal(clone.level, INFO) Fiber.new do assert_equal(logger.level, INFO) logger.with_level(:WARN) do assert_equal(other.level, ERROR) assert_equal(logger.level, WARN) + assert_equal(clone.level, INFO) + assert_equal(logger.dup.level, INFO) + assert_equal(logger.clone.level, INFO) end assert_equal(logger.level, INFO) end.resume @@ -67,20 +78,31 @@ def level_key logger.level = INFO # default level other = subclass.new(nil) other.level = ERROR # default level + clone = logger.clone # should not be chaged assert_equal(other.level, ERROR) logger.with_level(:WARN) do assert_equal(other.level, ERROR) assert_equal(logger.level, WARN) + assert_equal(clone.level, INFO) + assert_equal(logger.dup.level, INFO) + assert_equal(logger.clone.level, INFO) + logger.clone.with_level(:FATAL) do + assert_equal(logger.level, WARN) + end logger.with_level(DEBUG) do # verify reentrancy assert_equal(logger.level, DEBUG) + assert_equal(clone.level, INFO) Fiber.new do assert_equal(logger.level, DEBUG) logger.with_level(:WARN) do assert_equal(other.level, ERROR) assert_equal(logger.level, WARN) + assert_equal(clone.level, INFO) + assert_equal(logger.dup.level, INFO) + assert_equal(logger.clone.level, INFO) end assert_equal(logger.level, DEBUG) end.resume