async-safe 0.3.2 → 0.4.0

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: 7859608fbc62080a81bb9f20ce2c9ffe8f299f76432d1ec9f4a3d4aa47291240
4
+ data.tar.gz: e9a4f441e4ccdbfe0f086d6df486f22c7aa9504d07f42128689f54fd1befad4d
5
5
  SHA512:
6
- metadata.gz: e0a5426f5a9157f304e8e4d679199d5db4da2d82aba4322c84c3b67b1d61fb64c98c6370260d09f7a8615b8b785bd3691167e7ec897b31ee7d123b75e9ac2bc4
7
- data.tar.gz: 9d0101093bf612f7915083bf7e03bf2e1bc38d9cb3cae8dbde5e7ae0507926a4f47dba0b39592581fca00b1c556137f45fdb71ba9688d40d123e9cbf9232b921
6
+ metadata.gz: 12d90b93da4da2d525d6bf9e14acf235f0a2c50bc8301cd265b88f333a9d8c9af55ed0a7d69cb992239af715f5cd680650de0d2b111fb474c3a487f1007acfdb
7
+ data.tar.gz: 84d82aa42d46b54494ac5f4c8b025c80f41b2c8ccb7e1412d89a5d1073c662f9b86c7a84dce7c782c73e97164fc8a2df0a5d064bb4dac83a9e0de3d7a85cc264
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,34 @@
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
+ 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
+
11
39
  module Async
12
40
  module Safe
13
41
  # 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
@@ -87,17 +87,55 @@ module Async
87
87
 
88
88
  # Explicitly transfer ownership of objects to the current fiber.
89
89
  #
90
+ # Also recursively transfers ownership of any tracked instance variables and
91
+ # objects contained in collections (Array, Hash, Set).
92
+ #
90
93
  # @parameter objects [Array(Object)] The objects to transfer.
91
94
  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
+
92
106
  @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)
97
109
  end
98
110
  end
99
111
  end
100
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:
134
+ klass.async_safe_traverse(object) do |element|
135
+ traverse_objects(element, visited)
136
+ end
137
+ end
138
+
101
139
  # Check if the current access is allowed or constitutes a violation.
102
140
  #
103
141
  # @parameter trace_point [TracePoint] The trace point containing access information.
@@ -105,7 +143,7 @@ module Async
105
143
  object = trace_point.self
106
144
 
107
145
  # Skip tracking class/module methods:
108
- return if object.is_a?(Class) || object.is_a?(Module)
146
+ return if object.is_a?(Module)
109
147
 
110
148
  # Skip frozen objects:
111
149
  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.0"
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
@@ -1,2 +1 @@
1
- �br���b�ٚ�)O�zil����qaqT�`��ލ�c�ߴ��œu��u}(g �=F��j ����Z1�<����F��z�$�N�ia�>������Zebε
2
- ��cB4�V$h�0}�>cd��U@j�D�^Li�a@-�����!��J�!�{
1
+ '�i��HX���Kl4�fŷ�9�O�^��w|��P _]��@_AFW URHwvn9