async-safe 0.1.0 → 0.3.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 +37 -10
- data/lib/async/safe/builtins.rb +46 -11
- data/lib/async/safe/class.rb +30 -5
- data/lib/async/safe/version.rb +1 -1
- data/lib/async/safe.rb +3 -38
- data/readme.md +15 -0
- data/releases.md +15 -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: 8d77ebea20acb12bfc6f9e7848a809a17d993df98604ec9b457433ddf211437d
|
4
|
+
data.tar.gz: b7bdcdf3d68df5a5f385a252926319b6ffb2154b3bb431507d63fb3fe64f255e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9cc0287a91eb60e17f4a6047574a88bf813dbe39ddf345738d3aacadd8a171ed3540e2e7f1004c987c45a5abd3afdfe83407cec276f08ede0a612a76fd0509da
|
7
|
+
data.tar.gz: ada0c1d9c123abacc5688a787229a0c578d2b3c59b7dbd7fe2c944e990a9eb2ffa3de4842e43b22b3eb0521683a8c1f7ccfd5221be35df9f6d36ee16e3e33914
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/context/getting-started.md
CHANGED
@@ -25,12 +25,14 @@ 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
|
-
|
28
|
+
### Single-Owner Model (Opt-In)
|
29
29
|
|
30
|
-
By default, all
|
30
|
+
By default, all classes are assumed to be async-safe. To enable tracking for specific classes, mark them with `ASYNC_SAFE = false`:
|
31
31
|
|
32
32
|
~~~ ruby
|
33
33
|
class MyBody
|
34
|
+
ASYNC_SAFE = false # Enable tracking for this class
|
35
|
+
|
34
36
|
def initialize(chunks)
|
35
37
|
@chunks = chunks
|
36
38
|
@index = 0
|
@@ -51,7 +53,7 @@ Fiber.schedule do
|
|
51
53
|
end
|
52
54
|
~~~
|
53
55
|
|
54
|
-
|
56
|
+
### Marking Async-Safe Classes
|
55
57
|
|
56
58
|
Mark entire classes as safe for concurrent access:
|
57
59
|
|
@@ -90,15 +92,29 @@ class MyQueue
|
|
90
92
|
end
|
91
93
|
~~~
|
92
94
|
|
93
|
-
|
95
|
+
Or use a hash for per-method configuration:
|
96
|
+
|
97
|
+
~~~ ruby
|
98
|
+
class MixedClass
|
99
|
+
ASYNC_SAFE = {
|
100
|
+
read: true, # This method is async-safe
|
101
|
+
write: false # This method is NOT async-safe
|
102
|
+
}.freeze
|
103
|
+
|
104
|
+
# ... implementation
|
105
|
+
end
|
106
|
+
~~~
|
107
|
+
|
108
|
+
### Marking Methods with Hash
|
94
109
|
|
95
|
-
|
110
|
+
Use a hash to specify which methods are async-safe:
|
96
111
|
|
97
112
|
~~~ ruby
|
98
113
|
class MixedSafety
|
99
|
-
|
100
|
-
|
101
|
-
|
114
|
+
ASYNC_SAFE = {
|
115
|
+
safe_read: true, # This method is async-safe
|
116
|
+
increment: false # This method is NOT async-safe
|
117
|
+
}.freeze
|
102
118
|
|
103
119
|
def initialize(data)
|
104
120
|
@data = data
|
@@ -110,7 +126,7 @@ class MixedSafety
|
|
110
126
|
end
|
111
127
|
|
112
128
|
def increment
|
113
|
-
@count += 1 # Not async-safe
|
129
|
+
@count += 1 # Not async-safe - will be tracked
|
114
130
|
end
|
115
131
|
end
|
116
132
|
|
@@ -122,7 +138,18 @@ Fiber.schedule do
|
|
122
138
|
end
|
123
139
|
~~~
|
124
140
|
|
125
|
-
|
141
|
+
Or use an array to list async-safe methods:
|
142
|
+
|
143
|
+
~~~ ruby
|
144
|
+
class MyClass
|
145
|
+
ASYNC_SAFE = [:read, :inspect].freeze
|
146
|
+
|
147
|
+
# read and inspect are async-safe
|
148
|
+
# all other methods will be tracked
|
149
|
+
end
|
150
|
+
~~~
|
151
|
+
|
152
|
+
### Transferring Ownership
|
126
153
|
|
127
154
|
Explicitly transfer ownership between fibers:
|
128
155
|
|
data/lib/async/safe/builtins.rb
CHANGED
@@ -8,15 +8,50 @@
|
|
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
|
-
|
12
|
-
|
13
|
-
Thread::Queue
|
14
|
-
|
15
|
-
Thread::
|
16
|
-
|
11
|
+
module Async
|
12
|
+
module Safe
|
13
|
+
# Automatically transfers ownership of objects when they are removed from a Thread::Queue.
|
14
|
+
#
|
15
|
+
# When included in Thread::Queue or Thread::SizedQueue, this module wraps pop/deq/shift
|
16
|
+
# methods to automatically transfer ownership of the dequeued object to the fiber that
|
17
|
+
# dequeues it.
|
18
|
+
module TransferableThreadQueue
|
19
|
+
# Pop an object from the queue and transfer ownership to the current fiber.
|
20
|
+
#
|
21
|
+
# @parameter arguments [Array] Arguments passed to the original pop method.
|
22
|
+
# @returns [Object] The dequeued object with transferred ownership.
|
23
|
+
def pop(...)
|
24
|
+
object = super(...)
|
25
|
+
Async::Safe.transfer(object)
|
26
|
+
object
|
27
|
+
end
|
28
|
+
|
29
|
+
# Dequeue an object from the queue and transfer ownership to the current fiber.
|
30
|
+
#
|
31
|
+
# Alias for {#pop}.
|
32
|
+
#
|
33
|
+
# @parameter arguments [Array] Arguments passed to the original deq method.
|
34
|
+
# @returns [Object] The dequeued object with transferred ownership.
|
35
|
+
def deq(...)
|
36
|
+
object = super(...)
|
37
|
+
Async::Safe.transfer(object)
|
38
|
+
object
|
39
|
+
end
|
40
|
+
|
41
|
+
# Shift an object from the queue and transfer ownership to the current fiber.
|
42
|
+
#
|
43
|
+
# Alias for {#pop}.
|
44
|
+
#
|
45
|
+
# @parameter arguments [Array] Arguments passed to the original shift method.
|
46
|
+
# @returns [Object] The dequeued object with transferred ownership.
|
47
|
+
def shift(...)
|
48
|
+
object = super(...)
|
49
|
+
Async::Safe.transfer(object)
|
50
|
+
object
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
17
55
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
# ObjectSpace::WeakMap is async-safe:
|
22
|
-
ObjectSpace::WeakMap::ASYNC_SAFE = true
|
56
|
+
Thread::Queue.prepend(Async::Safe::TransferableThreadQueue)
|
57
|
+
Thread::SizedQueue.prepend(Async::Safe::TransferableThreadQueue)
|
data/lib/async/safe/class.rb
CHANGED
@@ -7,15 +7,40 @@
|
|
7
7
|
class Class
|
8
8
|
# Check if this class or a specific method is async-safe.
|
9
9
|
#
|
10
|
+
# The `ASYNC_SAFE` constant can be:
|
11
|
+
# - `true` - entire class is async-safe.
|
12
|
+
# - `false` - entire class is NOT async-safe (single-owner).
|
13
|
+
# - `{method_name: true/false}` - per-method configuration.
|
14
|
+
# - `[method_name1, method_name2]` - per-method configuration.
|
15
|
+
#
|
10
16
|
# @parameter method [Symbol | Nil] The method name to check, or nil to check if the entire class is async-safe.
|
11
|
-
# @returns [Boolean] Whether the class or method is async-safe.
|
17
|
+
# @returns [Boolean] Whether the class or method is async-safe. Defaults to true if not specified.
|
12
18
|
def async_safe?(method = nil)
|
13
|
-
|
14
|
-
|
15
|
-
|
19
|
+
if const_defined?(:ASYNC_SAFE)
|
20
|
+
async_safe = const_get(:ASYNC_SAFE)
|
21
|
+
|
22
|
+
case async_safe
|
23
|
+
when Hash
|
24
|
+
if method
|
25
|
+
async_safe = async_safe.fetch(method, false)
|
26
|
+
else
|
27
|
+
# In general, some methods may not be safe:
|
28
|
+
async_safe = false
|
29
|
+
end
|
30
|
+
when Array
|
31
|
+
if method
|
32
|
+
async_safe = async_safe.include?(method)
|
33
|
+
else
|
34
|
+
# In general, some methods may not be safe:
|
35
|
+
async_safe = false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
return async_safe
|
16
40
|
end
|
17
41
|
|
18
|
-
|
42
|
+
# Default to true:
|
43
|
+
return true
|
19
44
|
end
|
20
45
|
|
21
46
|
# Mark the class as async-safe or not.
|
data/lib/async/safe/version.rb
CHANGED
data/lib/async/safe.rb
CHANGED
@@ -12,47 +12,12 @@ require_relative "safe/builtins"
|
|
12
12
|
module Async
|
13
13
|
# Provides runtime thread safety monitoring for concurrent Ruby code.
|
14
14
|
#
|
15
|
-
# By default, all
|
16
|
-
#
|
17
|
-
#
|
15
|
+
# By default, all classes are assumed to be async-safe. Classes that follow a
|
16
|
+
# **single-owner model** should be explicitly marked with `ASYNC_SAFE = false` to
|
17
|
+
# enable tracking and violation detection.
|
18
18
|
#
|
19
19
|
# Enable monitoring in your test suite to catch concurrency bugs early.
|
20
20
|
module Safe
|
21
|
-
# Include this module to mark specific methods as async-safe
|
22
|
-
def self.included(base)
|
23
|
-
base.extend(ClassMethods)
|
24
|
-
end
|
25
|
-
|
26
|
-
# Class methods for marking async-safe methods
|
27
|
-
module ClassMethods
|
28
|
-
# Mark one or more methods as async-safe.
|
29
|
-
#
|
30
|
-
# @parameter method_names [Array(Symbol)] The methods to mark as async-safe.
|
31
|
-
def async_safe(*method_names)
|
32
|
-
@async_safe_methods ||= Set.new
|
33
|
-
@async_safe_methods.merge(method_names)
|
34
|
-
end
|
35
|
-
|
36
|
-
# Check if a method is async-safe.
|
37
|
-
#
|
38
|
-
# Overrides the default implementation from `Class` to also check method-level safety.
|
39
|
-
#
|
40
|
-
# @parameter method [Symbol | Nil] The method name to check, or nil to check if the entire class is async-safe.
|
41
|
-
# @returns [Boolean] Whether the method or class is async-safe.
|
42
|
-
def async_safe?(method = nil)
|
43
|
-
# Check if entire class is marked async-safe:
|
44
|
-
return true if super
|
45
|
-
|
46
|
-
# Check if specific method is marked async-safe:
|
47
|
-
if method
|
48
|
-
return @async_safe_methods&.include?(method)
|
49
|
-
end
|
50
|
-
|
51
|
-
# Default to false if no method is specified and the class is not async safe:
|
52
|
-
return false
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
21
|
class << self
|
57
22
|
# @attribute [Monitor] The global monitoring instance.
|
58
23
|
attr_reader :monitor
|
data/readme.md
CHANGED
@@ -22,6 +22,21 @@ 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.3.0
|
26
|
+
|
27
|
+
- Inverted default model: classes are async-safe by default, use `ASYNC_SAFE = false` to enable tracking.
|
28
|
+
- Added flexible `ASYNC_SAFE` constant support: boolean, hash, or array configurations.
|
29
|
+
- Added `Class#async_safe!` method for marking classes.
|
30
|
+
- Added `Class#async_safe?(method)` method for querying safety.
|
31
|
+
- Removed logger feature: always raises `ViolationError` exceptions.
|
32
|
+
- Removed `Async::Safe::Concurrent` module: use `async_safe!` instead.
|
33
|
+
- Removed `reset!` method: use `disable!` + `enable!` instead.
|
34
|
+
|
35
|
+
### v0.2.0
|
36
|
+
|
37
|
+
- `Thread::Queue` transfers ownership of objects popped from it.
|
38
|
+
- Add support for `logger:` option in `Async::Safe.enable!` which logs violations instead of raising errors.
|
39
|
+
|
25
40
|
### v0.1.0
|
26
41
|
|
27
42
|
- Implement TracePoint-based ownership tracking.
|
data/releases.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Releases
|
2
2
|
|
3
|
+
## v0.3.0
|
4
|
+
|
5
|
+
- Inverted default model: classes are async-safe by default, use `ASYNC_SAFE = false` to enable tracking.
|
6
|
+
- Added flexible `ASYNC_SAFE` constant support: boolean, hash, or array configurations.
|
7
|
+
- Added `Class#async_safe!` method for marking classes.
|
8
|
+
- Added `Class#async_safe?(method)` method for querying safety.
|
9
|
+
- Removed logger feature: always raises `ViolationError` exceptions.
|
10
|
+
- Removed `Async::Safe::Concurrent` module: use `async_safe!` instead.
|
11
|
+
- Removed `reset!` method: use `disable!` + `enable!` instead.
|
12
|
+
|
13
|
+
## v0.2.0
|
14
|
+
|
15
|
+
- `Thread::Queue` transfers ownership of objects popped from it.
|
16
|
+
- Add support for `logger:` option in `Async::Safe.enable!` which logs violations instead of raising errors.
|
17
|
+
|
3
18
|
## v0.1.0
|
4
19
|
|
5
20
|
- Implement TracePoint-based ownership tracking.
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
Binary file
|