Skip to content

Commit b77f223

Browse files
committed
CI fix
1 parent 52b1abd commit b77f223

File tree

2 files changed

+193
-6
lines changed

2 files changed

+193
-6
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
TEMPORAL_CLOUD_OPS_TEST_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}
8989
TEMPORAL_CLOUD_OPS_TEST_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
9090
TEMPORAL_CLOUD_OPS_TEST_API_VERSION: 2024-05-13-00
91-
run: bundle exec rake TESTOPTS="--verbose"
91+
run: bundle exec rake compile test TESTOPTS="--verbose --name=/^WorkerWorkflowTest#test_confirm_garbage_collect$/"
9292

9393
- name: Deploy docs
9494
# Only deploy on main merge, not in PRs

temporalio/test/worker_workflow_test.rb

Lines changed: 192 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'base64_codec'
44
require 'net/http'
5+
require 'objspace'
56
require 'temporalio/client'
67
require 'temporalio/testing'
78
require '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

Comments
 (0)