22
33require 'base64_codec'
44require 'net/http'
5+ require 'objspace'
56require 'temporalio/client'
67require 'temporalio/testing'
78require 'temporalio/worker'
@@ -1912,9 +1913,10 @@ def test_fail_workflow_payload_converter
19121913 class ConfirmGarbageCollectWorkflow < Temporalio ::Workflow ::Definition
19131914 @initialized_count = 0
19141915 @finalized_count = 0
1916+ @weak_instance = nil
19151917
19161918 class << self
1917- attr_accessor :initialized_count , :finalized_count
1919+ attr_accessor :initialized_count , :finalized_count , :weak_instance
19181920
19191921 def create_finalizer
19201922 proc { @finalized_count += 1 }
@@ -1923,6 +1925,7 @@ def create_finalizer
19231925
19241926 def initialize
19251927 self . class . initialized_count += 1
1928+ self . class . weak_instance = WeakRef . new ( self )
19261929 ObjectSpace . define_finalizer ( self , self . class . create_finalizer )
19271930 end
19281931
@@ -1940,10 +1943,194 @@ def test_confirm_garbage_collect
19401943 assert_equal 0 , ConfirmGarbageCollectWorkflow . finalized_count
19411944 end
19421945
1943- # Now with worker shutdown, GC and confirm finalized
1944- assert_eventually do
1945- GC . start
1946- assert_equal 1 , ConfirmGarbageCollectWorkflow . finalized_count
1946+ # Perform a GC and confirm gone
1947+ GC . start
1948+ begin
1949+ # Access instance and assert that it fails when a method is called on it as expected
1950+ instance = ConfirmGarbageCollectWorkflow . weak_instance . __getobj__
1951+
1952+ path , cat = find_retaining_path_to ( instance . object_id , max_depth : 12 )
1953+ print_annotated_path ( path , root_category : cat )
1954+ flunk
1955+ rescue WeakRef ::RefError
1956+ # Expected
1957+ end
1958+ end
1959+
1960+ # Find one path from any GC root to the object with +target_id+.
1961+ # Returns [path_array, root_category], where path_array is an array of objects from root..target.
1962+ def find_retaining_path_to ( target_id , max_depth : 12 , max_visits : 250_000 , category_whitelist : nil )
1963+ roots = ObjectSpace . reachable_objects_from_root # {category_sym => [objs]}
1964+ queue = [ ]
1965+ seen = { }
1966+ parent = { } # child_id -> parent_id
1967+ root_of = { } # obj_id -> root_category
1968+
1969+ roots . each do |category , objs |
1970+ next if category_whitelist && !category_whitelist . include? ( category )
1971+
1972+ objs . each do |o |
1973+ id = o . __id__
1974+ next if seen [ id ]
1975+
1976+ seen [ id ] = true
1977+ parent [ id ] = nil
1978+ root_of [ id ] = category
1979+ queue << o
1980+ end
1981+ end
1982+
1983+ visits = 0
1984+ depth = 0
1985+ level_remaining = queue . length
1986+ next_level = 0
1987+
1988+ found_leaf = nil
1989+
1990+ while !queue . empty? && visits < max_visits && depth <= max_depth
1991+ cur = queue . shift
1992+ level_remaining -= 1
1993+ visits += 1
1994+
1995+ cid = cur . __id__
1996+ if cid == target_id
1997+ found_leaf = cid
1998+ break
1999+ end
2000+
2001+ children = begin
2002+ ObjectSpace . reachable_objects_from ( cur )
2003+ rescue StandardError
2004+ nil
2005+ end
2006+
2007+ if children
2008+ children . each do |child |
2009+ chid = child . __id__
2010+ next if seen [ chid ]
2011+
2012+ seen [ chid ] = true
2013+ parent [ chid ] = cid
2014+ root_of [ chid ] ||= root_of [ cid ]
2015+ queue << child
2016+ next_level += 1
2017+ end
2018+ end
2019+
2020+ next unless level_remaining == 0
2021+
2022+ depth += 1
2023+ level_remaining = next_level
2024+ next_level = 0
2025+ end
2026+
2027+ return [ nil , nil ] unless found_leaf
2028+
2029+ # Reconstruct path
2030+ ids = [ ]
2031+ i = found_leaf
2032+ while i
2033+ ids << i
2034+ i = parent [ i ]
2035+ end
2036+ objs = ids . reverse . map do |id |
2037+ ObjectSpace . _id2ref ( id )
2038+ rescue StandardError
2039+ id
2040+ end
2041+ [ objs , root_of [ ids . first ] ]
2042+ end
2043+
2044+ # Label HOW +parent+ holds a reference to +child+ (ivar name, constant, index, etc.).
2045+ def edge_labels ( parent , child )
2046+ labels = [ ]
2047+ target = child
2048+
2049+ # 1) Instance variables (works for Class/Module too – class ivars are ivars on the Class object)
2050+ if parent . respond_to? ( :instance_variables )
2051+ parent . instance_variables . each do |ivar |
2052+ labels << "@#{ ivar . to_s . delete ( '@' ) } " if parent . instance_variable_get ( ivar ) . equal? ( target )
2053+ rescue StandardError
2054+ end
2055+ end
2056+
2057+ # 2) Class variables on Module/Class
2058+ if parent . is_a? ( Module )
2059+ parent . class_variables . each do |cvar |
2060+ labels << cvar . to_s if parent . class_variable_get ( cvar ) . equal? ( target )
2061+ rescue NameError
2062+ end
2063+ end
2064+
2065+ # 3) Constants on Module/Class (avoid triggering autoload)
2066+ if parent . is_a? ( Module )
2067+ parent . constants ( false ) . each do |c |
2068+ next if parent . respond_to? ( :autoload? ) && parent . autoload? ( c )
2069+
2070+ if parent . const_defined? ( c , false )
2071+ v = parent . const_get ( c , false )
2072+ labels << "::#{ c } " if v . equal? ( target )
2073+ end
2074+ rescue NameError , LoadError
2075+ end
2076+ end
2077+
2078+ # 4) Array elements
2079+ if parent . is_a? ( Array )
2080+ parent . each_with_index do |v , i |
2081+ labels << "[#{ i } ]" if v . equal? ( target )
2082+ end
2083+ end
2084+
2085+ # 5) Hash entries (key or value)
2086+ if parent . is_a? ( Hash )
2087+ parent . each do |k , v |
2088+ labels << "{key #{ k . inspect } }" if k . equal? ( target )
2089+ labels << "{value for #{ k . inspect } }" if v . equal? ( target )
2090+ end
2091+ end
2092+
2093+ # 6) Struct members
2094+ if parent . is_a? ( Struct )
2095+ parent . members . each do |m |
2096+ labels << ".#{ m } " if parent [ m ] . equal? ( target )
2097+ rescue StandardError
2098+ end
2099+ end
2100+
2101+ # 7) Fallback for VM internals
2102+ if labels . empty?
2103+ begin
2104+ labels << '(internal)' if parent . is_a? ( ObjectSpace ::InternalObjectWrapper )
2105+ rescue StandardError
2106+ end
2107+ end
2108+
2109+ labels . empty? ? [ '(unknown edge)' ] : labels
2110+ end
2111+
2112+ def describe_obj ( o )
2113+ cls = ( o . is_a? ( Module ) ? o : o . class )
2114+ "#<#{ cls } 0x#{ o . __id__ . to_s ( 16 ) } >"
2115+ rescue StandardError
2116+ o . inspect
2117+ end
2118+
2119+ def print_annotated_path ( path , root_category :)
2120+ puts "Retaining path (len=#{ path . length } ) from ROOT[:#{ root_category } ] to target:"
2121+ return if path . empty?
2122+
2123+ # First is the root
2124+ puts " ROOT[:#{ root_category } ] #{ describe_obj ( path . first ) } "
2125+ # Then edges with labels
2126+ ( 0 ...path . length - 1 ) . each do |i |
2127+ parent = path [ i ]
2128+ child = path [ i + 1 ]
2129+ labels = edge_labels ( parent , child )
2130+ labels . each_with_index do |lab , j |
2131+ arrow = ( j == 0 ? ' └─' : ' •' )
2132+ puts "#{ arrow } via #{ lab } → #{ describe_obj ( child ) } "
2133+ end
19472134 end
19482135 end
19492136
0 commit comments