async-safe 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d1ccb933e5a9b488f7c33e02d547ca69463e4b7c94fadccb3550196f9f765d5
4
- data.tar.gz: 1050434843c8f29403076ef9bf3aa0946122e9e62d68d6aea5334c17cd5a449c
3
+ metadata.gz: 72500ee1207a863df7f749896936bf9edce7e06f2f7d7a519329c0ada72951eb
4
+ data.tar.gz: 584c0393294503ea01cf1e1858f88f3c2faf5e2cd6fdf1b2f632af0708e4d787
5
5
  SHA512:
6
- metadata.gz: e0a5426f5a9157f304e8e4d679199d5db4da2d82aba4322c84c3b67b1d61fb64c98c6370260d09f7a8615b8b785bd3691167e7ec897b31ee7d123b75e9ac2bc4
7
- data.tar.gz: 9d0101093bf612f7915083bf7e03bf2e1bc38d9cb3cae8dbde5e7ae0507926a4f47dba0b39592581fca00b1c556137f45fdb71ba9688d40d123e9cbf9232b921
6
+ metadata.gz: 305ea4886f6b9fa0452de045244623806ed824ba24791ed0d2d1e6bf96a14f536f31fccbd54f099e5cc10ee1dc39067609be9b646daac413c47126a86f0306a2
7
+ data.tar.gz: 78dd7ab4aab4893c2824788e03522905698d6f400557976677253fbcc53643b4a0dbaaa8a8157249415f9380a317ffddc126948bb484fa2b6c795513abf4fc2d
checksums.yaml.gz.sig CHANGED
Binary file
@@ -163,6 +163,38 @@ Fiber.schedule do
163
163
  end
164
164
  ~~~
165
165
 
166
+ ### Deep Transfer with Traversal
167
+
168
+ By default, `transfer` only transfers the object itself (shallow). For collections like `Array`, `Hash`, and `Set`, the gem automatically traverses and transfers contained objects:
169
+
170
+ ~~~ ruby
171
+ bodies = [Body.new, Body.new]
172
+
173
+ Async::Safe.transfer(bodies) # Transfers array AND all bodies inside
174
+ ~~~
175
+
176
+ Custom classes can define traversal behavior using `async_safe_traverse`:
177
+
178
+ ~~~ ruby
179
+ class Request
180
+ async_safe!(false)
181
+ attr_accessor :body, :headers
182
+
183
+ def self.async_safe_traverse(instance, &block)
184
+ yield instance.body
185
+ yield instance.headers
186
+ end
187
+ end
188
+
189
+ request = Request.new
190
+ request.body = Body.new
191
+ request.headers = Headers.new
192
+
193
+ Async::Safe.transfer(request) # Transfers request, body, AND headers.
194
+ ~~~
195
+
196
+ **Note:** Shareable objects (`async_safe? -> true`) are never traversed or transferred, as they can be safely shared across fibers.
197
+
166
198
  ## Integration with Tests
167
199
 
168
200
  Add to your test helper (e.g., `config/sus.rb` or `spec/spec_helper.rb`):
@@ -8,6 +8,46 @@
8
8
  # Note: Immutable values (nil, true, false, integers, symbols, etc.) are already
9
9
  # handled by the frozen? check in the monitor and don't need to be listed here.
10
10
 
11
+ # Arrays contain references to other objects that may need transfer:
12
+ class Array
13
+ ASYNC_SAFE = false
14
+
15
+ # Traverse array elements during ownership transfer.
16
+ #
17
+ # @parameter instance [Array] The array instance to traverse.
18
+ # @parameter block [Proc] Block to call for each element.
19
+ def self.async_safe_traverse(instance, &block)
20
+ instance.each(&block)
21
+ end
22
+ end
23
+
24
+ # Hashes contain keys and values that may need transfer:
25
+ class Hash
26
+ ASYNC_SAFE = false
27
+
28
+ # Traverse hash keys and values during ownership transfer.
29
+ #
30
+ # @parameter instance [Hash] The hash instance to traverse.
31
+ # @parameter block [Proc] Block to call for each key and value.
32
+ def self.async_safe_traverse(instance, &block)
33
+ instance.each_key(&block)
34
+ instance.each_value(&block)
35
+ end
36
+ end
37
+
38
+ # Sets contain elements that may need transfer:
39
+ class Set
40
+ ASYNC_SAFE = false
41
+
42
+ # Traverse set elements during ownership transfer.
43
+ #
44
+ # @parameter instance [Set] The set instance to traverse.
45
+ # @parameter block [Proc] Block to call for each element.
46
+ def self.async_safe_traverse(instance, &block)
47
+ instance.each(&block)
48
+ end
49
+ end
50
+
11
51
  module Async
12
52
  module Safe
13
53
  # Automatically transfers ownership of objects when they are removed from a Thread::Queue.
@@ -45,9 +45,32 @@ class Class
45
45
 
46
46
  # Mark the class as async-safe or not.
47
47
  #
48
- # @parameter value [Boolean] Whether the class is async-safe.
49
- # @returns [Boolean] Whether the class is async-safe.
48
+ # @parameter value [Boolean | Hash | Array] Configuration for async-safety.
49
+ # @returns [Boolean | Hash | Array] The configured value.
50
50
  def async_safe!(value = true)
51
51
  self.const_set(:ASYNC_SAFE, value)
52
52
  end
53
+
54
+ # Define how to traverse this object's children during ownership transfer.
55
+ #
56
+ # This method is called by `Async::Safe.transfer` to recursively transfer
57
+ # ownership of contained objects. By default, only the object itself is transferred.
58
+ # Define this method to enable deep transfer for collection-like classes.
59
+ #
60
+ # @parameter instance [Object] The instance to traverse.
61
+ # @parameter block [Proc] Block to call for each child object that should be transferred.
62
+ #
63
+ # ~~~ ruby
64
+ # class MyContainer
65
+ # async_safe!(false)
66
+ # attr_reader :children
67
+ #
68
+ # def self.async_safe_traverse(instance, &block)
69
+ # instance.children.each(&block)
70
+ # end
71
+ # end
72
+ # ~~~
73
+ def async_safe_traverse(instance, &block)
74
+ # Default: no traversal (shallow transfer only)
75
+ end
53
76
  end
@@ -6,6 +6,9 @@
6
6
  require "set"
7
7
  require "weakref"
8
8
 
9
+ # Fiber-local variable to track when we're in a transfer operation:
10
+ Fiber.attr_accessor :async_safe_transfer
11
+
9
12
  module Async
10
13
  module Safe
11
14
  # Raised when an object is accessed from a different fiber than the one that owns it.
@@ -87,14 +90,55 @@ module Async
87
90
 
88
91
  # Explicitly transfer ownership of objects to the current fiber.
89
92
  #
93
+ # Also recursively transfers ownership of any tracked instance variables and
94
+ # objects contained in collections (Array, Hash, Set).
95
+ #
90
96
  # @parameter objects [Array(Object)] The objects to transfer.
91
97
  def transfer(*objects)
92
- @mutex.synchronize do
93
- current = Fiber.current
94
-
98
+ current = Fiber.current
99
+ visited = Set.new
100
+
101
+ # Disable tracking during traversal to avoid deadlock:
102
+ current.async_safe_transfer = true
103
+
104
+ begin
105
+ # Traverse object graph:
95
106
  objects.each do |object|
96
- @owners[object] = current
107
+ traverse_objects(object, visited)
108
+ end
109
+
110
+ # Transfer all visited objects:
111
+ @mutex.synchronize do
112
+ visited.each do |object|
113
+ @owners[object] = current if @owners.key?(object)
114
+ end
97
115
  end
116
+ ensure
117
+ current.async_safe_transfer = false
118
+ end
119
+ end
120
+
121
+ # Traverse the object graph and collect all reachable objects.
122
+ #
123
+ # @parameter object [Object] The object to traverse.
124
+ # @parameter visited [Set] Set of visited objects (object references, not IDs).
125
+ private def traverse_objects(object, visited)
126
+ # Avoid circular references:
127
+ return if visited.include?(object)
128
+
129
+ # Skip objects that don't need traversing:
130
+ return if object.frozen? or object.is_a?(Module)
131
+
132
+ # Skip async-safe (shareable) objects - they're not owned:
133
+ klass = object.class
134
+ return if klass.async_safe?(nil)
135
+
136
+ # Mark as visited:
137
+ visited << object
138
+
139
+ # Recurse through custom traversal:
140
+ klass.async_safe_traverse(object) do |element|
141
+ traverse_objects(element, visited)
98
142
  end
99
143
  end
100
144
 
@@ -102,10 +146,13 @@ module Async
102
146
  #
103
147
  # @parameter trace_point [TracePoint] The trace point containing access information.
104
148
  def check_access(trace_point)
149
+ # Skip if we're in a transfer operation:
150
+ return if Fiber.current.async_safe_transfer
151
+
105
152
  object = trace_point.self
106
153
 
107
154
  # Skip tracking class/module methods:
108
- return if object.is_a?(Class) || object.is_a?(Module)
155
+ return if object.is_a?(Module)
109
156
 
110
157
  # Skip frozen objects:
111
158
  return if object.frozen?
@@ -5,7 +5,7 @@
5
5
 
6
6
  module Async
7
7
  module Safe
8
- VERSION = "0.3.2"
8
+ VERSION = "0.4.1"
9
9
  end
10
10
  end
11
11
 
data/readme.md CHANGED
@@ -22,6 +22,10 @@ Please see the [project documentation](https://socketry.github.io/async-safe/) f
22
22
 
23
23
  Please see the [project releases](https://socketry.github.io/async-safe/releases/index) for all releases.
24
24
 
25
+ ### v0.4.0
26
+
27
+ - Improved `Async::Safe.transfer` to recursively transfer ownership of tracked instance variables.
28
+
25
29
  ### v0.3.2
26
30
 
27
31
  - Better error message.
@@ -32,6 +36,9 @@ Please see the [project releases](https://socketry.github.io/async-safe/releases
32
36
  - Added flexible `ASYNC_SAFE` constant support: boolean, hash, or array configurations.
33
37
  - Added `Class#async_safe!` method for marking classes.
34
38
  - Added `Class#async_safe?(method)` method for querying safety.
39
+ - Added `Class.async_safe_traverse` for custom deep transfer traversal (opt-in).
40
+ - Improved `Async::Safe.transfer` to use shallow transfer by default with opt-in deep traversal.
41
+ - Mark built-in collections (`Array`, `Hash`, `Set`) as single-owner with deep traversal support.
35
42
  - Removed logger feature: always raises `ViolationError` exceptions.
36
43
  - Removed `Async::Safe::Concurrent` module: use `async_safe!` instead.
37
44
 
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.4.0
4
+
5
+ - Improved `Async::Safe.transfer` to recursively transfer ownership of tracked instance variables.
6
+
3
7
  ## v0.3.2
4
8
 
5
9
  - Better error message.
@@ -10,6 +14,9 @@
10
14
  - Added flexible `ASYNC_SAFE` constant support: boolean, hash, or array configurations.
11
15
  - Added `Class#async_safe!` method for marking classes.
12
16
  - Added `Class#async_safe?(method)` method for querying safety.
17
+ - Added `Class.async_safe_traverse` for custom deep transfer traversal (opt-in).
18
+ - Improved `Async::Safe.transfer` to use shallow transfer by default with opt-in deep traversal.
19
+ - Mark built-in collections (`Array`, `Hash`, `Set`) as single-owner with deep traversal support.
13
20
  - Removed logger feature: always raises `ViolationError` exceptions.
14
21
  - Removed `Async::Safe::Concurrent` module: use `async_safe!` instead.
15
22
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-safe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file