async-safe 0.4.1 → 0.5.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +194 -188
- data/lib/async/safe/class.rb +0 -23
- data/lib/async/safe/monitor.rb +109 -122
- data/lib/async/safe/version.rb +1 -1
- data/lib/async/safe/violation_error.rb +52 -0
- data/lib/async/safe.rb +10 -17
- data/readme.md +7 -2
- data/releases.md +5 -0
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
- data/lib/async/safe/builtins.rb +0 -97
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a159f426b967f880b5935565fde44767b006695b75475e097f3e8d2e7239f8fe
|
|
4
|
+
data.tar.gz: a3549d80e371d6b42e6bacd9f6aa64f09479e8432e0648b8aa4b2a93c5e25f45
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b514ecda9b54080477fe913b9facaeba091523ba6d72dc2aa3a186c1c01fff74595057f64947fff3fcb570804166d273009518d3275861373cd5ccea21edc67
|
|
7
|
+
data.tar.gz: 4fe5eabb41e14c091176b1b30ee600db76143158db5a18bfa4b995d8e3a071b8d84e7857cb84a3443d70864cf36accc57ba514c857948fc18c9699bea88e443d
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/getting-started.md
CHANGED
|
@@ -25,31 +25,103 @@ Async::Safe.enable!
|
|
|
25
25
|
|
|
26
26
|
When a violation is detected, an `Async::Safe::ViolationError` will be raised immediately with details about the object, method, and execution contexts involved.
|
|
27
27
|
|
|
28
|
+
## Concurrent Access Detection
|
|
29
|
+
|
|
30
|
+
`async-safe` detects **concurrent access** (data races) to objects across fibers. Objects can move freely between fibers - violations are only raised when two fibers try to access the same object simultaneously.
|
|
31
|
+
|
|
32
|
+
~~~ ruby
|
|
33
|
+
Async::Safe.enable!
|
|
34
|
+
|
|
35
|
+
request = Request.new("http://example.com")
|
|
36
|
+
request.process # Main fiber
|
|
37
|
+
|
|
38
|
+
Fiber.new do
|
|
39
|
+
# No problem - sequential access is allowed
|
|
40
|
+
request.process # ✅ OK
|
|
41
|
+
end.resume
|
|
42
|
+
~~~
|
|
43
|
+
|
|
44
|
+
However, actual concurrent access is detected:
|
|
45
|
+
|
|
46
|
+
~~~ ruby
|
|
47
|
+
require 'async'
|
|
48
|
+
|
|
49
|
+
counter = Counter.new
|
|
50
|
+
counter.increment
|
|
51
|
+
|
|
52
|
+
Async do |task|
|
|
53
|
+
task.async do
|
|
54
|
+
counter.increment # Fiber A accessing
|
|
55
|
+
sleep 0.1 # ... method is still running
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
task.async do
|
|
59
|
+
sleep 0.05 # Wait for Fiber A to start
|
|
60
|
+
counter.increment # 💥 Concurrent access detected!
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
~~~
|
|
64
|
+
|
|
65
|
+
This approach focuses on catching **actual bugs** (data races) while allowing objects to move naturally between fibers.
|
|
66
|
+
|
|
67
|
+
### Guard-Based Concurrency
|
|
68
|
+
|
|
69
|
+
For objects with multiple independent operation types (like streams with separate read/write operations), `async_safe?` can return different guard symbols for different operations:
|
|
70
|
+
|
|
71
|
+
~~~ ruby
|
|
72
|
+
class Stream
|
|
73
|
+
def self.async_safe?(method)
|
|
74
|
+
case method
|
|
75
|
+
when :read then :readable
|
|
76
|
+
when :write then :writable
|
|
77
|
+
else false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def read; end
|
|
82
|
+
def write(data); end
|
|
83
|
+
end
|
|
84
|
+
~~~
|
|
85
|
+
|
|
86
|
+
This allows:
|
|
87
|
+
- ✅ Concurrent `read` and `write` (different guards: `:readable` and `:writable`)
|
|
88
|
+
- ❌ Concurrent `read` and `read` (same `:readable` guard)
|
|
89
|
+
- ❌ Concurrent `write` and `write` (same `:writable` guard)
|
|
90
|
+
|
|
91
|
+
Each guard can only be held by one fiber at a time, but different guards can be held concurrently.
|
|
92
|
+
|
|
28
93
|
### Single-Owner Model
|
|
29
94
|
|
|
30
95
|
By default, all classes are assumed to be async-safe. To enable tracking for specific classes, mark them with `ASYNC_SAFE = false`:
|
|
31
96
|
|
|
32
97
|
~~~ ruby
|
|
33
98
|
class MyBody
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
99
|
+
ASYNC_SAFE = false # Enable tracking for this class
|
|
100
|
+
|
|
101
|
+
def initialize(chunks)
|
|
102
|
+
@chunks = chunks
|
|
103
|
+
@index = 0
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def read
|
|
107
|
+
chunk = @chunks[@index]
|
|
108
|
+
@index += 1
|
|
109
|
+
chunk
|
|
110
|
+
end
|
|
46
111
|
end
|
|
47
112
|
|
|
48
113
|
body = MyBody.new(["a", "b", "c"])
|
|
49
|
-
body.read #
|
|
114
|
+
body.read # Main fiber
|
|
50
115
|
|
|
51
116
|
Fiber.schedule do
|
|
52
|
-
|
|
117
|
+
body.read # ✅ OK - sequential access is allowed
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# But concurrent access is detected:
|
|
121
|
+
require 'async'
|
|
122
|
+
Async do |task|
|
|
123
|
+
task.async { body.read } # Two fibers accessing
|
|
124
|
+
task.async { body.read } # at the same time → ViolationError!
|
|
53
125
|
end
|
|
54
126
|
~~~
|
|
55
127
|
|
|
@@ -59,26 +131,26 @@ Mark entire classes as safe for concurrent access:
|
|
|
59
131
|
|
|
60
132
|
~~~ ruby
|
|
61
133
|
class MyQueue
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
134
|
+
async_safe!
|
|
135
|
+
|
|
136
|
+
def initialize
|
|
137
|
+
@queue = Thread::Queue.new
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def push(item)
|
|
141
|
+
@queue.push(item)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def pop
|
|
145
|
+
@queue.pop
|
|
146
|
+
end
|
|
75
147
|
end
|
|
76
148
|
|
|
77
149
|
queue = MyQueue.new
|
|
78
150
|
queue.push("item")
|
|
79
151
|
|
|
80
152
|
Fiber.schedule do
|
|
81
|
-
|
|
153
|
+
queue.push("another") # ✅ OK - class is marked async-safe
|
|
82
154
|
end
|
|
83
155
|
~~~
|
|
84
156
|
|
|
@@ -86,9 +158,9 @@ Alternatively, you can manually set the constant:
|
|
|
86
158
|
|
|
87
159
|
~~~ ruby
|
|
88
160
|
class MyQueue
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
161
|
+
ASYNC_SAFE = true
|
|
162
|
+
|
|
163
|
+
# ... implementation
|
|
92
164
|
end
|
|
93
165
|
~~~
|
|
94
166
|
|
|
@@ -96,12 +168,12 @@ Or use a hash for per-method configuration:
|
|
|
96
168
|
|
|
97
169
|
~~~ ruby
|
|
98
170
|
class MixedClass
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
171
|
+
ASYNC_SAFE = {
|
|
172
|
+
read: true, # This method is async-safe
|
|
173
|
+
write: false # This method is NOT async-safe
|
|
174
|
+
}.freeze
|
|
175
|
+
|
|
176
|
+
# ... implementation
|
|
105
177
|
end
|
|
106
178
|
~~~
|
|
107
179
|
|
|
@@ -111,30 +183,30 @@ Use a hash to specify which methods are async-safe:
|
|
|
111
183
|
|
|
112
184
|
~~~ ruby
|
|
113
185
|
class MixedSafety
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
186
|
+
ASYNC_SAFE = {
|
|
187
|
+
safe_read: true, # This method is async-safe
|
|
188
|
+
increment: false # This method is NOT async-safe
|
|
189
|
+
}.freeze
|
|
190
|
+
|
|
191
|
+
def initialize(data)
|
|
192
|
+
@data = data
|
|
193
|
+
@count = 0
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def safe_read
|
|
197
|
+
@data # Async-safe method
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def increment
|
|
201
|
+
@count += 1 # Not async-safe - will be tracked
|
|
202
|
+
end
|
|
131
203
|
end
|
|
132
204
|
|
|
133
205
|
obj = MixedSafety.new("data")
|
|
134
206
|
|
|
135
207
|
Fiber.schedule do
|
|
136
|
-
|
|
137
|
-
|
|
208
|
+
obj.safe_read # ✅ OK - method is marked async-safe
|
|
209
|
+
obj.increment # 💥 Raises Async::Safe::ViolationError!
|
|
138
210
|
end
|
|
139
211
|
~~~
|
|
140
212
|
|
|
@@ -142,58 +214,13 @@ Or use an array to list async-safe methods:
|
|
|
142
214
|
|
|
143
215
|
~~~ ruby
|
|
144
216
|
class MyClass
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
217
|
+
ASYNC_SAFE = [:read, :inspect].freeze
|
|
218
|
+
|
|
219
|
+
# read and inspect are async-safe
|
|
220
|
+
# all other methods will be tracked
|
|
149
221
|
end
|
|
150
222
|
~~~
|
|
151
223
|
|
|
152
|
-
### Transferring Ownership
|
|
153
|
-
|
|
154
|
-
Explicitly transfer ownership between fibers:
|
|
155
|
-
|
|
156
|
-
~~~ ruby
|
|
157
|
-
request = create_request
|
|
158
|
-
process_in_main_fiber(request)
|
|
159
|
-
|
|
160
|
-
Fiber.schedule do
|
|
161
|
-
Async::Safe.transfer(request) # Transfer ownership
|
|
162
|
-
process_in_worker_fiber(request) # ✅ OK now
|
|
163
|
-
end
|
|
164
|
-
~~~
|
|
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
224
|
|
|
198
225
|
## Integration with Tests
|
|
199
226
|
|
|
@@ -222,37 +249,37 @@ When deciding whether to mark a class or method with `ASYNC_SAFE = false`, consi
|
|
|
222
249
|
**Immutable objects:**
|
|
223
250
|
~~~ ruby
|
|
224
251
|
class ImmutableUser
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
252
|
+
def initialize(name, email)
|
|
253
|
+
@name = name.freeze
|
|
254
|
+
@email = email.freeze
|
|
255
|
+
freeze # Entire object is frozen
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
attr_reader :name, :email
|
|
232
259
|
end
|
|
233
260
|
~~~
|
|
234
261
|
|
|
235
262
|
**Pure functions (no state modification):**
|
|
236
263
|
~~~ ruby
|
|
237
264
|
class Calculator
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
265
|
+
def add(a, b)
|
|
266
|
+
a + b # No instance state, pure computation
|
|
267
|
+
end
|
|
241
268
|
end
|
|
242
269
|
~~~
|
|
243
270
|
|
|
244
271
|
**Thread-safe synchronization:**
|
|
245
272
|
~~~ ruby
|
|
246
273
|
class SafeQueue
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
274
|
+
ASYNC_SAFE = true # Explicitly marked
|
|
275
|
+
|
|
276
|
+
def initialize
|
|
277
|
+
@queue = Thread::Queue.new # Thread-safe internally
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def push(item)
|
|
281
|
+
@queue.push(item) # Delegates to thread-safe queue
|
|
282
|
+
end
|
|
256
283
|
end
|
|
257
284
|
~~~
|
|
258
285
|
|
|
@@ -261,44 +288,44 @@ end
|
|
|
261
288
|
**Mutable instance state:**
|
|
262
289
|
~~~ ruby
|
|
263
290
|
class Counter
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
ASYNC_SAFE = false # Enable tracking
|
|
292
|
+
|
|
293
|
+
def initialize
|
|
294
|
+
@count = 0
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def increment
|
|
298
|
+
@count += 1 # Reads and writes @count (race condition!)
|
|
299
|
+
end
|
|
273
300
|
end
|
|
274
301
|
~~~
|
|
275
302
|
|
|
276
303
|
**Stateful iteration:**
|
|
277
304
|
~~~ ruby
|
|
278
305
|
class Reader
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
306
|
+
ASYNC_SAFE = false # Enable tracking
|
|
307
|
+
|
|
308
|
+
def initialize(data)
|
|
309
|
+
@data = data
|
|
310
|
+
@index = 0
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def read
|
|
314
|
+
value = @data[@index]
|
|
315
|
+
@index += 1 # Mutates state
|
|
316
|
+
value
|
|
317
|
+
end
|
|
291
318
|
end
|
|
292
319
|
~~~
|
|
293
320
|
|
|
294
321
|
**Lazy initialization:**
|
|
295
322
|
~~~ ruby
|
|
296
323
|
class DataLoader
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
324
|
+
ASYNC_SAFE = false # Enable tracking
|
|
325
|
+
|
|
326
|
+
def data
|
|
327
|
+
@data ||= load_data # Not atomic! (race condition)
|
|
328
|
+
end
|
|
302
329
|
end
|
|
303
330
|
~~~
|
|
304
331
|
|
|
@@ -308,23 +335,23 @@ Use hash or array configuration for classes with both safe and unsafe methods:
|
|
|
308
335
|
|
|
309
336
|
~~~ ruby
|
|
310
337
|
class MixedClass
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
338
|
+
ASYNC_SAFE = {
|
|
339
|
+
read_config: true, # Safe: only reads frozen data
|
|
340
|
+
update_state: false # Unsafe: modifies mutable state
|
|
341
|
+
}.freeze
|
|
342
|
+
|
|
343
|
+
def initialize
|
|
344
|
+
@config = {setting: "value"}.freeze
|
|
345
|
+
@state = {count: 0}
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def read_config
|
|
349
|
+
@config[:setting] # Safe: frozen hash
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def update_state
|
|
353
|
+
@state[:count] += 1 # Unsafe: mutates state
|
|
354
|
+
end
|
|
328
355
|
end
|
|
329
356
|
~~~
|
|
330
357
|
|
|
@@ -351,24 +378,3 @@ If you're unsure whether a class is thread-safe:
|
|
|
351
378
|
2. **Run your tests** with monitoring enabled.
|
|
352
379
|
3. **If no violations occur** after thorough testing, it's likely safe.
|
|
353
380
|
4. **Review the code** for the patterns above.
|
|
354
|
-
|
|
355
|
-
## How It Works
|
|
356
|
-
|
|
357
|
-
1. **Default Assumption**: All objects follow a single-owner model (not thread-safe).
|
|
358
|
-
2. **TracePoint Monitoring**: Tracks which fiber/thread first accesses each object.
|
|
359
|
-
3. **Violation Detection**: Raises an exception when a different fiber/thread accesses the same object.
|
|
360
|
-
4. **Explicit Safety**: Objects/methods can be marked as thread-safe to allow concurrent access.
|
|
361
|
-
5. **Zero Overhead**: Monitoring is only active when explicitly enabled.
|
|
362
|
-
|
|
363
|
-
## Use Cases
|
|
364
|
-
|
|
365
|
-
- **Detecting concurrency bugs** in development and testing.
|
|
366
|
-
- **Validating thread safety assumptions** in async/fiber-based code.
|
|
367
|
-
- **Finding race conditions** before they cause production issues.
|
|
368
|
-
- **Educational tool** for learning about thread safety in Ruby.
|
|
369
|
-
|
|
370
|
-
## Performance
|
|
371
|
-
|
|
372
|
-
- **Zero overhead when disabled** - TracePoint is not activated.
|
|
373
|
-
- **Minimal overhead when enabled** - suitable for development/test environments.
|
|
374
|
-
- **Not recommended for production** - use only in development/testing.
|
data/lib/async/safe/class.rb
CHANGED
|
@@ -50,27 +50,4 @@ class Class
|
|
|
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
|
|
76
53
|
end
|
data/lib/async/safe/monitor.rb
CHANGED
|
@@ -3,81 +3,42 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
require "weakref"
|
|
8
|
-
|
|
9
|
-
# Fiber-local variable to track when we're in a transfer operation:
|
|
10
|
-
Fiber.attr_accessor :async_safe_transfer
|
|
6
|
+
require_relative "violation_error"
|
|
11
7
|
|
|
12
8
|
module Async
|
|
13
9
|
module Safe
|
|
14
|
-
#
|
|
15
|
-
class ViolationError < StandardError
|
|
16
|
-
# Initialize a new violation error.
|
|
17
|
-
#
|
|
18
|
-
# @parameter message [String | Nil] Optional custom message.
|
|
19
|
-
# @parameter target [Object] The object that was accessed.
|
|
20
|
-
# @parameter method [Symbol] The method that was called.
|
|
21
|
-
# @parameter owner [Fiber] The fiber that owns the object.
|
|
22
|
-
# @parameter current [Fiber] The fiber that attempted to access the object.
|
|
23
|
-
def initialize(message = nil, target:, method:, owner:, current:)
|
|
24
|
-
@target = target
|
|
25
|
-
@method = method
|
|
26
|
-
@owner = owner
|
|
27
|
-
@current = current
|
|
28
|
-
|
|
29
|
-
super(message || build_message)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
attr_reader :object_class, :method, :owner, :current
|
|
33
|
-
|
|
34
|
-
# Convert the violation error to a JSON-serializable hash.
|
|
35
|
-
#
|
|
36
|
-
# @returns [Hash] A hash representation of the violation.
|
|
37
|
-
def as_json
|
|
38
|
-
{
|
|
39
|
-
object_class: @object_class,
|
|
40
|
-
method: @method,
|
|
41
|
-
owner: {
|
|
42
|
-
name: @owner.inspect,
|
|
43
|
-
backtrace: @owner.backtrace,
|
|
44
|
-
},
|
|
45
|
-
current: {
|
|
46
|
-
name: @current.inspect,
|
|
47
|
-
backtrace: @current.backtrace,
|
|
48
|
-
},
|
|
49
|
-
}
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private def build_message
|
|
53
|
-
"Thread safety violation detected!\n\tObject: #{@target.inspect}##{@method}\n\tOwner: #{@owner.inspect}\n\tAccessed by: #{@current.inspect}"
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# The core monitoring implementation using TracePoint.
|
|
10
|
+
# Monitors for concurrent access to objects across fibers.
|
|
58
11
|
#
|
|
59
|
-
# This
|
|
60
|
-
#
|
|
61
|
-
#
|
|
12
|
+
# This monitor detects when multiple fibers try to execute methods on the same
|
|
13
|
+
# object simultaneously (actual data races). Sequential access across fibers is
|
|
14
|
+
# allowed - objects can be passed between fibers freely.
|
|
62
15
|
#
|
|
63
|
-
#
|
|
64
|
-
# and maintains a registry of which fiber "owns" each object. Uses weak references
|
|
65
|
-
# to avoid preventing garbage collection of tracked objects.
|
|
16
|
+
# Uses TracePoint to track in-flight method calls and detect concurrent access.
|
|
66
17
|
class Monitor
|
|
67
18
|
ASYNC_SAFE = true
|
|
68
19
|
|
|
69
|
-
# Initialize a new monitor
|
|
20
|
+
# Initialize a new concurrency monitor.
|
|
70
21
|
def initialize
|
|
71
|
-
@
|
|
22
|
+
@guards = ObjectSpace::WeakMap.new # Tracks {object => fiber} or {object => {guard => fiber}}
|
|
72
23
|
@mutex = Thread::Mutex.new
|
|
73
24
|
@trace_point = nil
|
|
74
25
|
end
|
|
75
26
|
|
|
76
|
-
attr :
|
|
27
|
+
attr :guards
|
|
77
28
|
|
|
78
29
|
# Enable the monitor by activating the TracePoint.
|
|
79
30
|
def enable!
|
|
80
|
-
@trace_point
|
|
31
|
+
return if @trace_point
|
|
32
|
+
|
|
33
|
+
@trace_point = TracePoint.new(:call, :return) do |tp|
|
|
34
|
+
if tp.event == :call
|
|
35
|
+
check_call(tp)
|
|
36
|
+
else
|
|
37
|
+
check_return(tp)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@trace_point.enable
|
|
81
42
|
end
|
|
82
43
|
|
|
83
44
|
# Disable the monitor by deactivating the TracePoint.
|
|
@@ -88,67 +49,94 @@ module Async
|
|
|
88
49
|
end
|
|
89
50
|
end
|
|
90
51
|
|
|
91
|
-
#
|
|
52
|
+
# Transfer has no effect in concurrency monitoring.
|
|
92
53
|
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
54
|
+
# Objects can move freely between fibers. This method exists for
|
|
55
|
+
# backward compatibility but does nothing.
|
|
95
56
|
#
|
|
96
|
-
# @parameter objects [Array(Object)] The objects to transfer.
|
|
57
|
+
# @parameter objects [Array(Object)] The objects to transfer (ignored).
|
|
97
58
|
def transfer(*objects)
|
|
98
|
-
|
|
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:
|
|
106
|
-
objects.each do |object|
|
|
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
|
|
115
|
-
end
|
|
116
|
-
ensure
|
|
117
|
-
current.async_safe_transfer = false
|
|
118
|
-
end
|
|
59
|
+
# No-op - objects move freely between fibers
|
|
119
60
|
end
|
|
120
61
|
|
|
121
|
-
#
|
|
62
|
+
# Check method call for concurrent access violations.
|
|
122
63
|
#
|
|
123
|
-
# @parameter
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
64
|
+
# @parameter trace_point [TracePoint] The trace point containing call information.
|
|
65
|
+
private def check_call(trace_point)
|
|
66
|
+
object = trace_point.self
|
|
67
|
+
|
|
68
|
+
# Skip tracking class/module methods:
|
|
69
|
+
return if object.is_a?(Module)
|
|
70
|
+
|
|
71
|
+
# Skip frozen objects:
|
|
72
|
+
return if object.frozen?
|
|
128
73
|
|
|
129
|
-
|
|
130
|
-
return if object.frozen? or object.is_a?(Module)
|
|
74
|
+
method = trace_point.method_id
|
|
131
75
|
|
|
132
|
-
#
|
|
76
|
+
# Check the object's actual class:
|
|
133
77
|
klass = object.class
|
|
134
|
-
return if klass.async_safe?(nil)
|
|
135
78
|
|
|
136
|
-
#
|
|
137
|
-
|
|
79
|
+
# Check if the class or method is marked as async-safe:
|
|
80
|
+
# Returns: true (skip), false (simple tracking), or Symbol (guard-based tracking)
|
|
81
|
+
safe = klass.async_safe?(method)
|
|
82
|
+
return if safe == true
|
|
83
|
+
|
|
84
|
+
current = Fiber.current
|
|
138
85
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
86
|
+
@mutex.synchronize do
|
|
87
|
+
if safe == false
|
|
88
|
+
# Simple tracking (single guard)
|
|
89
|
+
if fiber = @guards[object]
|
|
90
|
+
if fiber != current && !fiber.is_a?(Hash)
|
|
91
|
+
# Concurrent access detected!
|
|
92
|
+
raise ViolationError.new(
|
|
93
|
+
"Concurrent access detected!",
|
|
94
|
+
target: object,
|
|
95
|
+
method: method,
|
|
96
|
+
owner: fiber,
|
|
97
|
+
current: current,
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
# Acquire the guard
|
|
102
|
+
@guards[object] = current
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
# Multi-guard tracking
|
|
106
|
+
guard = safe
|
|
107
|
+
|
|
108
|
+
# Get or create the guards hash for this object
|
|
109
|
+
entry = @guards[object]
|
|
110
|
+
if entry.nil? || !entry.is_a?(Hash)
|
|
111
|
+
guards = @guards[object] = {}
|
|
112
|
+
else
|
|
113
|
+
guards = entry
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check if another fiber currently holds this guard
|
|
117
|
+
if fiber = guards[guard]
|
|
118
|
+
if fiber != current
|
|
119
|
+
# Concurrent access detected within the same guard!
|
|
120
|
+
raise ViolationError.new(
|
|
121
|
+
"Concurrent access detected (guard: #{guard})!",
|
|
122
|
+
target: object,
|
|
123
|
+
method: method,
|
|
124
|
+
owner: fiber,
|
|
125
|
+
current: current,
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
else
|
|
129
|
+
# Acquire this guard
|
|
130
|
+
guards[guard] = current
|
|
131
|
+
end
|
|
132
|
+
end
|
|
142
133
|
end
|
|
143
134
|
end
|
|
144
135
|
|
|
145
|
-
# Check
|
|
136
|
+
# Check method return to release guard.
|
|
146
137
|
#
|
|
147
|
-
# @parameter trace_point [TracePoint] The trace point containing
|
|
148
|
-
def
|
|
149
|
-
# Skip if we're in a transfer operation:
|
|
150
|
-
return if Fiber.current.async_safe_transfer
|
|
151
|
-
|
|
138
|
+
# @parameter trace_point [TracePoint] The trace point containing return information.
|
|
139
|
+
private def check_return(trace_point)
|
|
152
140
|
object = trace_point.self
|
|
153
141
|
|
|
154
142
|
# Skip tracking class/module methods:
|
|
@@ -158,33 +146,32 @@ module Async
|
|
|
158
146
|
return if object.frozen?
|
|
159
147
|
|
|
160
148
|
method = trace_point.method_id
|
|
161
|
-
klass = trace_point.defined_class
|
|
162
149
|
|
|
163
150
|
# Check the object's actual class:
|
|
164
151
|
klass = object.class
|
|
165
152
|
|
|
166
153
|
# Check if the class or method is marked as async-safe:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
end
|
|
154
|
+
safe = klass.async_safe?(method)
|
|
155
|
+
return if safe == true
|
|
170
156
|
|
|
171
|
-
# Track ownership:
|
|
172
157
|
current = Fiber.current
|
|
173
158
|
|
|
174
159
|
@mutex.synchronize do
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
owner: owner,
|
|
182
|
-
current: current,
|
|
183
|
-
)
|
|
184
|
-
end
|
|
160
|
+
entry = @guards[object]
|
|
161
|
+
|
|
162
|
+
if safe == false
|
|
163
|
+
# Simple tracking (single guard)
|
|
164
|
+
# Release if this fiber holds it
|
|
165
|
+
@guards.delete(object) if entry == current
|
|
185
166
|
else
|
|
186
|
-
#
|
|
187
|
-
|
|
167
|
+
# Multi-guard tracking
|
|
168
|
+
guard = safe
|
|
169
|
+
|
|
170
|
+
if entry.is_a?(Hash)
|
|
171
|
+
entry.delete(guard) if entry[guard] == current
|
|
172
|
+
# Clean up empty guards hash
|
|
173
|
+
@guards.delete(object) if entry.empty?
|
|
174
|
+
end
|
|
188
175
|
end
|
|
189
176
|
end
|
|
190
177
|
end
|
data/lib/async/safe/version.rb
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Async
|
|
7
|
+
module Safe
|
|
8
|
+
# Raised when an object is accessed from a different fiber than the one that owns it.
|
|
9
|
+
class ViolationError < StandardError
|
|
10
|
+
# Initialize a new violation error.
|
|
11
|
+
#
|
|
12
|
+
# @parameter message [String | Nil] Optional custom message.
|
|
13
|
+
# @parameter target [Object] The object that was accessed.
|
|
14
|
+
# @parameter method [Symbol] The method that was called.
|
|
15
|
+
# @parameter owner [Fiber] The fiber that owns the object.
|
|
16
|
+
# @parameter current [Fiber] The fiber that attempted to access the object.
|
|
17
|
+
def initialize(message = nil, target:, method:, owner:, current:)
|
|
18
|
+
@target = target
|
|
19
|
+
@method = method
|
|
20
|
+
@owner = owner
|
|
21
|
+
@current = current
|
|
22
|
+
|
|
23
|
+
super(message || build_message)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :object_class, :method, :owner, :current
|
|
27
|
+
|
|
28
|
+
# Convert the violation error to a JSON-serializable hash.
|
|
29
|
+
#
|
|
30
|
+
# @returns [Hash] A hash representation of the violation.
|
|
31
|
+
def as_json
|
|
32
|
+
{
|
|
33
|
+
object_class: @object_class,
|
|
34
|
+
method: @method,
|
|
35
|
+
owner: {
|
|
36
|
+
name: @owner.inspect,
|
|
37
|
+
backtrace: @owner.backtrace,
|
|
38
|
+
},
|
|
39
|
+
current: {
|
|
40
|
+
name: @current.inspect,
|
|
41
|
+
backtrace: @current.backtrace,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private def build_message
|
|
47
|
+
"Thread safety violation detected!\n\tObject: #{@target.inspect}##{@method}\n\tOwner: #{@owner.inspect}\n\tAccessed by: #{@current.inspect}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
data/lib/async/safe.rb
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
require_relative "safe/version"
|
|
7
7
|
require_relative "safe/class"
|
|
8
8
|
require_relative "safe/monitor"
|
|
9
|
-
require_relative "safe/builtins"
|
|
10
9
|
|
|
11
10
|
# @namespace
|
|
12
11
|
module Async
|
|
@@ -24,8 +23,11 @@ module Async
|
|
|
24
23
|
|
|
25
24
|
# Enable thread safety monitoring.
|
|
26
25
|
#
|
|
27
|
-
# This activates a TracePoint that
|
|
28
|
-
# There is no performance overhead when monitoring is disabled.
|
|
26
|
+
# This activates a TracePoint that detects concurrent access to objects across
|
|
27
|
+
# fibers and threads. There is no performance overhead when monitoring is disabled.
|
|
28
|
+
#
|
|
29
|
+
# Objects can move freely between fibers - only actual concurrent access (data races)
|
|
30
|
+
# is detected and reported.
|
|
29
31
|
def enable!
|
|
30
32
|
@monitor ||= Monitor.new
|
|
31
33
|
@monitor.enable!
|
|
@@ -37,23 +39,14 @@ module Async
|
|
|
37
39
|
@monitor = nil
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# This allows an object to be safely passed between fibers.
|
|
42
|
+
# Transfer has no effect in concurrency monitoring mode.
|
|
43
43
|
#
|
|
44
|
-
#
|
|
44
|
+
# Objects can move freely between fibers. This method is kept for
|
|
45
|
+
# backward compatibility but does nothing.
|
|
45
46
|
#
|
|
46
|
-
#
|
|
47
|
-
# request = Request.new(...)
|
|
48
|
-
#
|
|
49
|
-
# Fiber.schedule do
|
|
50
|
-
# # Transfer ownership of the request to this fiber:
|
|
51
|
-
# Async::Safe.transfer(request)
|
|
52
|
-
# process(request)
|
|
53
|
-
# end
|
|
54
|
-
# ~~~
|
|
47
|
+
# @parameter objects [Array(Object)] The objects to transfer (ignored).
|
|
55
48
|
def transfer(*objects)
|
|
56
|
-
|
|
49
|
+
# No-op - objects can move freely between fibers
|
|
57
50
|
end
|
|
58
51
|
end
|
|
59
52
|
end
|
data/readme.md
CHANGED
|
@@ -8,9 +8,9 @@ This gem provides a TracePoint-based ownership tracking system that detects when
|
|
|
8
8
|
|
|
9
9
|
## Motivation
|
|
10
10
|
|
|
11
|
-
Ruby's fiber-based concurrency (via `async`)
|
|
11
|
+
Ruby's fiber-based concurrency (via `async`) can lead to data races when objects are accessed concurrently. This gem helps you catch these concurrency bugs in your test suite by detecting when multiple fibers access the same object simultaneously.
|
|
12
12
|
|
|
13
|
-
Enable it in your tests to get immediate feedback when
|
|
13
|
+
Enable it in your tests to get immediate feedback when invalid concurrent access occurs.
|
|
14
14
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
@@ -22,6 +22,11 @@ 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.5.0
|
|
26
|
+
|
|
27
|
+
- More conservative tracking of objects using call/return for ownership transfer.
|
|
28
|
+
- Introduced guard concept for splitting ownership within a single object, e.g. independently concurrent readable and writable parts of an object.
|
|
29
|
+
|
|
25
30
|
### v0.4.0
|
|
26
31
|
|
|
27
32
|
- Improved `Async::Safe.transfer` to recursively transfer ownership of tracked instance variables.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.5.0
|
|
4
|
+
|
|
5
|
+
- More conservative tracking of objects using call/return for ownership transfer.
|
|
6
|
+
- Introduced guard concept for splitting ownership within a single object, e.g. independently concurrent readable and writable parts of an object.
|
|
7
|
+
|
|
3
8
|
## v0.4.0
|
|
4
9
|
|
|
5
10
|
- Improved `Async::Safe.transfer` to recursively transfer ownership of tracked instance variables.
|
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.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -45,10 +45,10 @@ files:
|
|
|
45
45
|
- context/getting-started.md
|
|
46
46
|
- context/index.yaml
|
|
47
47
|
- lib/async/safe.rb
|
|
48
|
-
- lib/async/safe/builtins.rb
|
|
49
48
|
- lib/async/safe/class.rb
|
|
50
49
|
- lib/async/safe/monitor.rb
|
|
51
50
|
- lib/async/safe/version.rb
|
|
51
|
+
- lib/async/safe/violation_error.rb
|
|
52
52
|
- license.md
|
|
53
53
|
- readme.md
|
|
54
54
|
- releases.md
|
metadata.gz.sig
CHANGED
|
Binary file
|
data/lib/async/safe/builtins.rb
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
5
|
-
|
|
6
|
-
# Mark Ruby's built-in thread-safe classes as async-safe
|
|
7
|
-
#
|
|
8
|
-
# Note: Immutable values (nil, true, false, integers, symbols, etc.) are already
|
|
9
|
-
# handled by the frozen? check in the monitor and don't need to be listed here.
|
|
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
|
-
|
|
51
|
-
module Async
|
|
52
|
-
module Safe
|
|
53
|
-
# Automatically transfers ownership of objects when they are removed from a Thread::Queue.
|
|
54
|
-
#
|
|
55
|
-
# When included in Thread::Queue or Thread::SizedQueue, this module wraps pop/deq/shift
|
|
56
|
-
# methods to automatically transfer ownership of the dequeued object to the fiber that
|
|
57
|
-
# dequeues it.
|
|
58
|
-
module TransferableThreadQueue
|
|
59
|
-
# Pop an object from the queue and transfer ownership to the current fiber.
|
|
60
|
-
#
|
|
61
|
-
# @parameter arguments [Array] Arguments passed to the original pop method.
|
|
62
|
-
# @returns [Object] The dequeued object with transferred ownership.
|
|
63
|
-
def pop(...)
|
|
64
|
-
object = super(...)
|
|
65
|
-
Async::Safe.transfer(object)
|
|
66
|
-
object
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Dequeue an object from the queue and transfer ownership to the current fiber.
|
|
70
|
-
#
|
|
71
|
-
# Alias for {#pop}.
|
|
72
|
-
#
|
|
73
|
-
# @parameter arguments [Array] Arguments passed to the original deq method.
|
|
74
|
-
# @returns [Object] The dequeued object with transferred ownership.
|
|
75
|
-
def deq(...)
|
|
76
|
-
object = super(...)
|
|
77
|
-
Async::Safe.transfer(object)
|
|
78
|
-
object
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Shift an object from the queue and transfer ownership to the current fiber.
|
|
82
|
-
#
|
|
83
|
-
# Alias for {#pop}.
|
|
84
|
-
#
|
|
85
|
-
# @parameter arguments [Array] Arguments passed to the original shift method.
|
|
86
|
-
# @returns [Object] The dequeued object with transferred ownership.
|
|
87
|
-
def shift(...)
|
|
88
|
-
object = super(...)
|
|
89
|
-
Async::Safe.transfer(object)
|
|
90
|
-
object
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
Thread::Queue.prepend(Async::Safe::TransferableThreadQueue)
|
|
97
|
-
Thread::SizedQueue.prepend(Async::Safe::TransferableThreadQueue)
|