Skip to content

Commit af63539

Browse files
Better ownership transfer.
1 parent d38758b commit af63539

File tree

5 files changed

+114
-5
lines changed

5 files changed

+114
-5
lines changed

guides/getting-started/readme.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ Fiber.schedule do
163163
end
164164
~~~
165165

166+
Transfer automatically recurses through instance variables:
167+
168+
~~~ ruby
169+
# If request contains a body that's also being tracked:
170+
request = Request.new
171+
request.body = Body.new # Body is tracked
172+
173+
Async::Safe.transfer(request) # Transfers both request AND body
174+
~~~
175+
166176
## Integration with Tests
167177

168178
Add to your test helper (e.g., `config/sus.rb` or `spec/spec_helper.rb`):

lib/async/safe/builtins.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,34 @@
88
# Note: Immutable values (nil, true, false, integers, symbols, etc.) are already
99
# handled by the frozen? check in the monitor and don't need to be listed here.
1010

11+
# Arrays contain references to other objects that may need transfer:
12+
class Array
13+
ASYNC_SAFE = false
14+
15+
def self.async_safe_traverse(instance, &block)
16+
instance.each(&block)
17+
end
18+
end
19+
20+
# Hashes contain keys and values that may need transfer:
21+
class Hash
22+
ASYNC_SAFE = false
23+
24+
def self.async_safe_traverse(instance, &block)
25+
instance.each_key(&block)
26+
instance.each_value(&block)
27+
end
28+
end
29+
30+
# Sets contain elements that may need transfer:
31+
class Set
32+
ASYNC_SAFE = false
33+
34+
def self.async_safe_traverse(instance, &block)
35+
instance.each(&block)
36+
end
37+
end
38+
1139
module Async
1240
module Safe
1341
# Automatically transfers ownership of objects when they are removed from a Thread::Queue.

lib/async/safe/monitor.rb

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,53 @@ def disable!
8787

8888
# Explicitly transfer ownership of objects to the current fiber.
8989
#
90+
# Also recursively transfers ownership of any tracked instance variables and
91+
# objects contained in collections (Array, Hash, Set).
92+
#
9093
# @parameter objects [Array(Object)] The objects to transfer.
9194
def transfer(*objects)
95+
current = Fiber.current
96+
visited = Set.new
97+
98+
# Traverse object graph (outside mutex to avoid deadlock):
99+
objects.each do |object|
100+
traverse_objects(object, visited)
101+
end
102+
103+
# Transfer all visited objects (convert to array to avoid triggering TracePoint in sync block):
104+
objects_to_transfer = visited.to_a
105+
92106
@mutex.synchronize do
93-
current = Fiber.current
94-
95-
objects.each do |object|
96-
@owners[object] = current
107+
objects_to_transfer.each do |object|
108+
@owners[object] = current if @owners.key?(object)
109+
end
110+
end
111+
end
112+
113+
private
114+
115+
# Traverse the object graph and collect all reachable objects.
116+
#
117+
# @parameter object [Object] The object to traverse.
118+
# @parameter visited [Set] Set of visited objects (object references, not IDs).
119+
def traverse_objects(object, visited)
120+
# Avoid circular references:
121+
return if visited.include?(object)
122+
123+
# Skip objects that don't need traversing:
124+
return if object.frozen? or object.is_a?(Module)
125+
126+
# Skip async-safe (shareable) objects - they're not owned:
127+
klass = object.class
128+
return if klass.async_safe?(nil)
129+
130+
# Mark as visited:
131+
visited << object
132+
133+
# Recurse through custom traversal if defined:
134+
if klass.respond_to?(:async_safe_traverse)
135+
klass.async_safe_traverse(object) do |element|
136+
traverse_objects(element, visited)
97137
end
98138
end
99139
end
@@ -105,7 +145,7 @@ def check_access(trace_point)
105145
object = trace_point.self
106146

107147
# Skip tracking class/module methods:
108-
return if object.is_a?(Class) || object.is_a?(Module)
148+
return if object.is_a?(Module)
109149

110150
# Skip frozen objects:
111151
return if object.frozen?

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Improved `Async::Safe.transfer` to recursively transfer ownership of tracked instance variables.
6+
37
## v0.3.2
48

59
- Better error message.

test/async/safe.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,33 @@ def push(item)
7373
end.not.to raise_exception
7474
end
7575

76+
it "recursively transfers owned instance variables" do
77+
inner_class = Class.new do
78+
async_safe!(false)
79+
def read; "data"; end
80+
end
81+
82+
outer_class = Class.new do
83+
async_safe!(false)
84+
attr_reader :inner
85+
def initialize(inner); @inner = inner; end
86+
end
87+
88+
inner = inner_class.new
89+
outer = outer_class.new(inner)
90+
91+
# Both owned by main fiber:
92+
inner.read
93+
outer.inner
94+
95+
Fiber.new do
96+
Async::Safe.transfer(outer)
97+
98+
# Both outer and inner should be transferred:
99+
outer.inner.read
100+
end.resume
101+
end
102+
76103
it "allows access after ownership transfer" do
77104
body = body_class.new(["a", "b"])
78105
body.read # Main fiber owns it

0 commit comments

Comments
 (0)