Skip to content

Instantly share code, notes, and snippets.

@tenderlove
Created January 29, 2025 17:27
Show Gist options
  • Save tenderlove/bf1c5c15c5b3d8f28f0ae6e4e8b36513 to your computer and use it in GitHub Desktop.
Save tenderlove/bf1c5c15c5b3d8f28f0ae6e4e8b36513 to your computer and use it in GitHub Desktop.
require "set"
require "rbconfig"
class Wrapper
def initialize
@a = Set.new
@b = Set.new
end
def write_a
# We would like a warning here
@a << "a"
end
def write_b
# We would like a warning here as well
@b << "b"
end
end
# No warnings are emitted until the warning is enabled
obj = Wrapper.new
Thread.new {
obj.write_a # no warning
obj.write_b # no warning
}.join
# Enable the warning
Warning[:non_owner_thread_writes] = true
obj = Wrapper.new
Thread.new {
obj.write_a
obj.write_b
}.join
# We would expect `write_a` and `write_b` to be blamed because they need a
# lock, but the output actually looks like this:
#
# /Users/aaron/git/ruby/lib/set.rb:515: warning: Hash mutated from a thread that didn't create it
# /Users/aaron/git/ruby/lib/set.rb:515: warning: Hash mutated from a thread that didn't create it
# /Users/aaron/git/ruby/lib/set.rb:515: warning: Hash mutated from a thread that didn't create it
# /Users/aaron/git/ruby/lib/set.rb:515: warning: Hash mutated from a thread that didn't create it
#
# It's because Set is implemented in Ruby, so it gets "blamed" for writing
# to the hash, but actually we need to do locking in `write_a` and `write_b`.
# To deal with this, we need to filter the warnings. Below is an example for
# filtering the warnings. It might need more logic for our app, but hopefully
# it can get the point across
module Warning
class << self
prepend Module.new {
def warn(msg, category:)
# We only care about non-owner writes
return super unless category == :non_owner_thread_writes
uplevel = 1
# Add some filtering logic here
caller_locations.each do |loc|
path = loc.path
# If the path starts with either of these directories, then we need
# to go up a frame.
if path.start_with?(RbConfig::CONFIG["topdir"]) || path.start_with?(RbConfig::CONFIG["rubylibdir"])
uplevel += 1
else
break
end
end
# If the first frame is _not_ in the above paths, then just emit
return super if uplevel == 1
# Otherwise, call Kernel.warn with the appropriate uplevel
Kernel.warn(msg, uplevel:, category:)
end
}
end
end
obj = Wrapper.new
Thread.new {
obj.write_a
obj.write_b
}.join
# Now the output looks like this:
#
# ./test.rb:12: warning: /Users/aaron/git/ruby/lib/set.rb:515: warning: Hash mutated from a thread that didn't create it
# ./test.rb:12: warning: /Users/aaron/git/ruby/lib/set.rb:515: warning: Hash mutated from a thread that didn't create it
# ./test.rb:17: warning: /Users/aaron/git/ruby/lib/set.rb:515: warning: Hash mutated from a thread that didn't create it
# ./test.rb:17: warning: /Users/aaron/git/ruby/lib/set.rb:515: warning: Hash mutated from a thread that didn't create it
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment