Skip to content

[Feature #21028] ObjectSpace#find_paths_to_unshareable_objects #13963

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[Feature #21028] ObjectSpace#find_paths_to_unshareable_objects
Add a method to find paths to Ractor-unshareable objects which can be
traced from an object.

Example:

    class Container
      attr_reader :value
      def initialize(value)
        @value = value
      end
    end

    mutable_string = "hello"
    container = Container.new(mutable_string)

    pp ObjectSpace.find_paths_to_unshareable_objects(container).to_a
      #=> [
        [#<Container:0x00007fc35843e388 @value="hello">],
        [#<Container:0x00007fc35843e388 @value="hello">, "hello"]
      ]

Co-authored-by: Yusuke Endoh <mame@ruby-lang.org>
  • Loading branch information
osyoyu and mame committed Jul 24, 2025
commit a7efcfbe9325dd8a9541ccf4f638335e26ecc78a
51 changes: 51 additions & 0 deletions ext/objspace/lib/objspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,55 @@ def dump_shapes(output: :file, since: 0)
return nil if output == :stdout
ret
end

# call-seq:
# ObjectSpace.find_paths_to_unshareable_objects(obj) {|path| ... } -> nil
# ObjectSpace.find_paths_to_unshareable_objects(obj) -> enumerator
#
# Finds all unshareable objects reachable from +obj+.
#
# When called with a block, yields an array representing the path from +obj+ to
# each unshareable object found. The path includes all intermediate objects
# traversed, ending with the unshareable object itself.
#
# If +obj+ itself is shareable, no paths are yielded.
#
# Example:
#
# class Container
# attr_reader :value
# def initialize(value)
# @value = value
# end
# end
#
# mutable_string = "hello"
# container = Container.new(mutable_string)
#
# pp ObjectSpace.find_paths_to_unshareable_objects(container).to_a
# #=> [
# [#<Container:0x00007fc35843e388 @value="hello">],
# [#<Container:0x00007fc35843e388 @value="hello">, "hello"]
# ]
def find_paths_to_unshareable_objects(obj)
return to_enum(__method__, obj) if !block_given?

queue = [[obj, []]]
visited = Set.new

while current = queue.shift
current_obj, current_path = current
visited.add(current_obj.object_id)

if !Ractor.shareable?(current_obj)
yield current_path + [current_obj]

ObjectSpace.reachable_objects_from(current_obj).each do |reachable|
if !reachable.is_a?(ObjectSpace::InternalObjectWrapper) && !visited.include?(reachable.object_id)
queue.push([reachable, current_path + [current_obj]])
end
end
end
end
end
end
38 changes: 38 additions & 0 deletions test/objspace/test_ractor.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "test/unit"
require "objspace"

class TestObjSpaceRactor < Test::Unit::TestCase
def test_tracing_does_not_crash
Expand Down Expand Up @@ -52,4 +53,41 @@ def fin
ractors.each(&:join)
RUBY
end

def test_find_paths_to_unshareable_objects
# Direct shareable object
assert_equal([], ObjectSpace.find_paths_to_unshareable_objects(1).to_a)

# Direct unshareable object
assert_equal([["unfrozen"]], ObjectSpace.find_paths_to_unshareable_objects("unfrozen").to_a)

# Hash containing unshareable object
obj = { a: 1, b: "frozen".freeze, c: "unfrozen" }
paths = ObjectSpace.find_paths_to_unshareable_objects(obj).to_a
assert_include(paths, [obj])
assert_include(paths, [obj, "unfrozen"])

# Array containing unshareable object
obj = [1, 2, "unfrozen", "frozen".freeze]
paths = ObjectSpace.find_paths_to_unshareable_objects(obj).to_a
assert_include(paths, [obj])
assert_include(paths, [obj, "unfrozen"])

# Custom class
klass = Class.new do
attr_accessor :value
end
obj = klass.new
obj.value = "unfrozen"
paths = ObjectSpace.find_paths_to_unshareable_objects(obj).to_a
assert_include(paths, [obj])
assert_include(paths, [obj, "unfrozen"])

# Circular reference
obj1 = { name: "obj1" }
obj2 = { name: "obj2", ref: obj1 }
obj1[:ref] = obj2
paths = ObjectSpace.find_paths_to_unshareable_objects(obj1).to_a
assert_include(paths, [obj1, obj2, "obj2"]) # does not circle back to obj1
end
end
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy