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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +32 -0
- data/lib/async/safe/builtins.rb +40 -0
- data/lib/async/safe/class.rb +25 -2
- data/lib/async/safe/monitor.rb +52 -5
- data/lib/async/safe/version.rb +1 -1
- data/readme.md +7 -0
- data/releases.md +7 -0
- data.tar.gz.sig +0 -0
- metadata +1 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72500ee1207a863df7f749896936bf9edce7e06f2f7d7a519329c0ada72951eb
|
4
|
+
data.tar.gz: 584c0393294503ea01cf1e1858f88f3c2faf5e2cd6fdf1b2f632af0708e4d787
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 305ea4886f6b9fa0452de045244623806ed824ba24791ed0d2d1e6bf96a14f536f31fccbd54f099e5cc10ee1dc39067609be9b646daac413c47126a86f0306a2
|
7
|
+
data.tar.gz: 78dd7ab4aab4893c2824788e03522905698d6f400557976677253fbcc53643b4a0dbaaa8a8157249415f9380a317ffddc126948bb484fa2b6c795513abf4fc2d
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/context/getting-started.md
CHANGED
@@ -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`):
|
data/lib/async/safe/builtins.rb
CHANGED
@@ -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.
|
data/lib/async/safe/class.rb
CHANGED
@@ -45,9 +45,32 @@ class Class
|
|
45
45
|
|
46
46
|
# Mark the class as async-safe or not.
|
47
47
|
#
|
48
|
-
# @parameter value [Boolean
|
49
|
-
# @returns [Boolean
|
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
|
data/lib/async/safe/monitor.rb
CHANGED
@@ -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
|
-
|
93
|
-
|
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
|
-
|
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?(
|
155
|
+
return if object.is_a?(Module)
|
109
156
|
|
110
157
|
# Skip frozen objects:
|
111
158
|
return if object.frozen?
|
data/lib/async/safe/version.rb
CHANGED
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
metadata.gz.sig
CHANGED
Binary file
|